Skip to content

Session与Turn状态

从真实字段、锁和消费者理解 SessionState、ActiveTurn 与 TurnState 的职责边界及状态生效时机。

基于rust-v0.150.0
CodexRustRuntime

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 字段。

rust
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。

rust
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 时归入默认 codex bucket。
  • granted_permissions_by_environment_id 是会话范围授权,并按 environment ID 隔离。

3. TurnState 字段 ​

源码位置:codex-rs/core/src/state/turn.rs :: ActiveTurn、TurnState。

rust
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。

rust
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

rust
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

rust
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

rust
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. 状态更新验证 ​

  1. 说明为什么 Session.state、Session.active_turn 和 ActiveTurn.turn_state 必须使用不同锁,而不能把所有字段合并到一个结构体。
  2. 遇到“权限响应写入了错误 Turn”时,检查响应保存的 originating_turn_state、PermissionGrantScope 和当前 active Turn 是否发生切换。

可以用下面的只读搜索把本文的 session and turn state 主线落回源码:

bash
rg -n "SessionState|TurnState|ActiveTurn|RunningTask|MailboxDeliveryPhase" codex-rs/core/src