Skip to content

Session核心数据结构

逐字段解析 Session、SessionConfiguration 与 SessionServices,说明锁、原子量、OnceLock 和后台资源的不变量。

基于rust-v0.150.0
CodexRustSessionConcurrency

Session核心数据结构 ​

Session 是一个 initialized model agent 的 Thread 级运行时。它必须同时支持用户输入、模型采样、工具 执行、MCP refresh、Realtime、持久化和配置更新,却不能把所有操作串行化在一个巨型 mutex 中。

源码的解法不是“尽量少用锁”,而是按生命周期选择同步原语:跨 Turn 事实进入 SessionState,当前 执行进入 ActiveTurn,一次性决策进入 OnceLock,刷新过程进入 semaphore/atomic,后台 worker 用 CancellationToken 与独立 handle 管理,长寿命依赖则集中在 SessionServices。

阅读本文前,先用 Core运行时架构总览 建立职责域,再理解 Thread与Turn概念模型 中 Thread、Turn 和 Task 的区别,以及 CodexThread公共API 中 CodexThread 与 Session 的所有权关系。 本文不逐项解释每个配置字段的业务含义,而是回答四个更基础的问题:状态应该放在哪一层、谁可以修改它、 一次异步操作能持有哪把锁,以及旧任务为什么不能清理新任务的状态。

Core运行时架构总览 负责组件关系与主控制流;本文只负责 Session 字段、所有权原语和同步不变量。若问题已经离开 Session 内部进入 App Server、模型 transport 或平台 sandbox,应返回架构总览选择下游专题。

读完后,应能从字段类型反推出生命周期,在 SessionState 与 TurnState 之间正确放置新状态,并能用源码 测试判断“值相等”和“仍是同一个运行时对象”是不是同一件事。

1. 所有权字段 ​

逐字段从上往下读容易丢失设计意图。下面的所有权图把 Session 本体和五种生命周期容器对应起来, 先回答“字段属于哪个作用域”,再进入完整源码。

图中的分支是所有权作用域,不是调用顺序。它帮助读者先问“谁会修改、需要等待什么、何时释放”, 而不是只看 Rust 类型是否实现 Clone。

2. Session字段分组 ​

源码位置: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>,
    // state 只保存跨 Turn 的可变事实,不承担活动 task 或外部 service owner。
    pub(super) state: Mutex<SessionState>,
    // 网络代理 rebuild 可能跨 await,使用单 permit semaphore 与 state 短锁分离。
    pub(super) managed_network_proxy_refresh_lock: Semaphore,
    // effective feature set 在 Session 生命周期内保持不变。
    pub(super) features: ManagedFeatures,
    pub(crate) windows_sandbox_proxy_settings_mode:
        codex_sandboxing::WindowsSandboxProxySettingsMode,
    pub(super) multi_agent_version: OnceLock<MultiAgentVersion>,
    // MCP refresh 自己组合 dirty atomic 与 publish gate,不借用 SessionState mutex。
    pub(super) mcp_refresh: McpRefresh,
    pub(super) mcp_elicitation_reviewer_handle: OnceLock<codex_mcp::ElicitationReviewerHandle>,
    pub(super) mcp_elicitation_lifecycle_handle: OnceLock<codex_mcp::ElicitationLifecycle>,
    pub(super) mcp_prewarm_tx: async_channel::Sender<()>,
    pub(super) mcp_prewarm_shutdown: CancellationToken,
    pub(super) mcp_prewarm_task: std::sync::Mutex<Option<JoinHandle<()>>>,
    pub(crate) conversation: Arc<RealtimeConversationManager>,
    // 单活 task 的权威槽位独立于 history/config state。
    pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
    pub(crate) async_hook_results: async_channel::Receiver<HookCompletedEvent>,
    pub(crate) input_queue: InputQueue,
    pub(crate) guardian_review_session: GuardianReviewSessionManager,
    pub(crate) services: SessionServices,
    pub(super) git_enrichment_policy: GitEnrichmentPolicy,
    pub(super) fork_persistence: ForkPersistence,
    pub(super) next_internal_sub_id: AtomicU64,
}
字段组字段写入频率主要不变量
身份thread_id、installation_id构造一次不随 Turn 改变
输出tx_event、agent_status高频Event 是流,status 只保留最新值
跨 Turn statestate高频短临界区history/config/window 保持一致快照
网络刷新managed_network_proxy_refresh_lock配置变化时同时最多一个 rebuild/apply cycle
Featurefeatures构造一次Session-static,不随 reload 整体替换
Multi-agentmulti_agent_version最多初始化一次同一 Thread 后续模型选择不得反复翻转
MCPmcp_refresh、prewarm 三字段invalidation/refreshcorrectness refresh 与 best-effort prewarm 分开
Realtimeconversation按 conversation 生命周期manager 可被多个方法共享
活动工作active_turn每个 task同一 Session 最多一个 running task
输入等待admissions、input_queue每条输入admission、mailbox 与 active Turn 正确归属
服务services大部分构造一次service owner 跨 Turn 存活
内部 IDnext_internal_sub_id内部 Turn/refresh原子递增且不与外部 UUID 混用

构造时这些容器被一次性装配,初值也体现语义:active_turn=None、prewarm channel 容量 1、网络刷新 semaphore 只有一个 permit、内部 ID 从 0 开始。

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

rust
let (mcp_prewarm_tx, mcp_prewarm_rx) = async_channel::bounded(1);
let sess = Arc::new(Session {
    thread_id,
    installation_id,
    tx_event: tx_event.clone(),
    agent_status,
    state: Mutex::new(state),
    managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
    features: config.features.clone(),
    windows_sandbox_proxy_settings_mode,
    multi_agent_version,
    mcp_refresh: McpRefresh::new(),
    mcp_elicitation_reviewer_handle: OnceLock::new(),
    mcp_elicitation_lifecycle_handle: OnceLock::new(),
    mcp_prewarm_tx,
    mcp_prewarm_shutdown: CancellationToken::new(),
    mcp_prewarm_task: std::sync::Mutex::new(None),
    conversation: Arc::new(RealtimeConversationManager::new()),
    // task 只能经统一 start/spawn 路径安装到空槽位。
    active_turn: Mutex::new(None),
    async_hook_results,
    input_queue: InputQueue::new(),
    guardian_review_session: GuardianReviewSessionManager::default(),
    services,
    git_enrichment_policy,
    fork_persistence,
    next_internal_sub_id: AtomicU64::new(0),
});

容量 1 的 prewarm queue 用于合并重复请求,不承担每次 refresh 的精确排队;真正的 MCP 正确性路径仍会 在需要时检查 dirty state。

3. 三个内层对象承担 ​

下面的类图展示 Session 与 SessionState、ActiveTurn、SessionServices 的组合。三者不是为了拆文件 而拆分,而是拥有不同锁范围和生命周期。

SessionState 可以在没有活动 Turn 时存在;ActiveTurn 只描述当前执行;SessionServices 即使 state 暂时锁住,也不应被迫跟着同一个 guard 访问。

3.1 SessionState ​

SessionState 包含 configuration、history、rate limits、auto-compaction window、额外上下文、connector selection、已授予环境权限和 startup prewarm。它的更新通常采用短锁:clone 所需 snapshot,释放锁, 再执行网络、文件或进程 I/O。

3.2 ActiveTurn单活 ​

Mutex<Option<ActiveTurn>> 的 None 表示没有当前执行切片;Some 内的 task 又可以在安装/清理阶段 暂时为 None。因此 active_turn.is_some() 不等价于模型正在采样,历史 Turn 更不保存在这里。

3.3 Session依赖容器 ​

SessionServices 不使用 HashMap<TypeId, Any> 一类通用 DI。每个字段都有编译期类型和生命周期:MCP runtime 是 live connection owner,model client 跨 Turn 复用,ThreadStore/LiveThread 管持久化,extension registry 在宿主装配后保持不可变。

源码位置:codex-rs/core/src/state/service.rs :: SessionServices(节选)

rust
pub(crate) struct SessionServices {
    // 每个 Thread 只有一个 live MCP connection owner。
    pub(crate) mcp_runtime: Arc<McpRuntime>,
    pub(crate) mcp_handler_cache: McpHandlerCache,
    pub(crate) unified_exec_manager: UnifiedExecProcessManager,
    pub(crate) elicitations: ElicitationService,
    pub(crate) analytics_events_client: AnalyticsEventsClient,
    // Hook 与 network proxy 允许原子热替换,不需要拿 SessionState 锁读服务。
    pub(crate) hooks: ArcSwap<Hooks>,
    pub(crate) rollout_thread_trace: ThreadTraceContext,
    pub(crate) user_shell: Arc<crate::shell::Shell>,
    pub(crate) exec_policy: Arc<ExecPolicyManager>,
    pub(crate) auth_manager: Arc<AuthManager>,
    pub(crate) openai_file_upload_client_pool: RouteAwareClientPool,
    pub(crate) models_manager: SharedModelsManager,
    pub(crate) session_telemetry: SessionTelemetry,
    pub(crate) tool_approvals: Mutex<ApprovalStore>,
    pub(crate) guardian_rejection_circuit_breaker: Mutex<GuardianRejectionCircuitBreaker>,
    pub(crate) runtime_handle: Handle,
    pub(crate) skills_service: Arc<HostSkillsService>,
    pub(crate) agents_md_manager: Arc<AgentsMdManager>,
    pub(crate) plugins_manager: Arc<PluginsManager>,
    pub(crate) mcp_manager: Arc<McpManager>,
    pub(crate) extensions: Arc<ExtensionRegistry<crate::config::Config>>,
    pub(crate) session_extension_data: ExtensionData,
    pub(crate) thread_extension_data: ExtensionData,
    pub(crate) client_mcp_extensions: ClientMcpExtensions,
    pub(crate) selected_capability_roots: Vec<SelectedCapabilityRoot>,
    pub(crate) agent_control: AgentControl,
    pub(crate) network_proxy: ArcSwapOption<StartedNetworkProxy>,
    pub(crate) state_db: Option<StateDbHandle>,
    pub(crate) live_thread: Option<LiveThread>,
    pub(crate) thread_store: Arc<dyn ThreadStore>,
    pub(crate) model_client: ModelClient,
    pub(crate) executed_tool_calls: Option<Arc<ExecutedToolCallRecorder>>,
    pub(crate) code_mode_service: CodeModeService,
    pub(crate) tool_search_handler_cache: ToolSearchHandlerCache,
    pub(crate) turn_environments: Arc<ThreadEnvironments>,
    // ... 其余 extension data、audit/provider 与工具缓存字段。
}

局部 mutex 也按领域拆开:tool_approvals 不应阻塞 guardian circuit breaker,二者更不能与 history/config 共享一把锁。

4. Session配置 ​

SessionConfiguration 位于 SessionState 内,类型实现 Clone。更新不是在多个 await 之间逐字段 修改当前对象,而是根据 SessionSettingsUpdate 生成 candidate configuration,通过约束后整体替换。

源码位置:codex-rs/core/src/session/session.rs :: SessionConfiguration(节选)

rust
pub(crate) struct SessionConfiguration {
    pub(super) provider: SharedModelProvider,
    pub(super) collaboration_mode: CollaborationMode,
    pub(super) model_reasoning_summary: Option<ReasoningSummaryConfig>,
    pub(super) service_tier: Option<String>,
    pub(super) developer_instructions: Option<String>,
    pub(super) personality: Option<Personality>,
    pub(super) base_instructions: String,
    pub(super) compact_prompt: Option<String>,
    pub(super) approval_policy: Constrained<AskForApproval>,
    pub(super) approvals_reviewer: ApprovalsReviewer,
    // permission profile、active profile ID 与profile roots必须作为一个状态整体更新。
    pub(super) permission_profile_state: PermissionProfileState,
    pub(super) windows_sandbox_level: WindowsSandboxLevel,
    pub(super) environments: TurnEnvironmentSelections,
    pub(super) codex_home: AbsolutePathBuf,
    pub(super) thread_name: Option<String>,
    pub(super) original_config_do_not_use: Arc<Config>,
    pub(super) metrics_service_name: Option<String>,
    pub(super) app_server_client_name: Option<String>,
    pub(super) app_server_client_version: Option<String>,
    pub(super) session_source: SessionSource,
    pub(super) history_mode: ThreadHistoryMode,
    pub(super) forked_from_thread_id: Option<ThreadId>,
    pub(super) parent_thread_id: Option<ThreadId>,
    pub(super) thread_source: Option<ThreadSource>,
    pub(super) originator: String,
    pub(super) dynamic_tools: Vec<DynamicToolSpec>,
    pub(super) user_shell_override: Option<shell::Shell>,
}

字段可以分为五个一致性组:

组字段为什么要一起看
模型语义provider、collaboration、reasoning、service tier、personality决定下一 Turn 的模型请求
指令base/developer/compact prompt、dynamic tools决定 model-visible context
权限approval、reviewer、permission state、Windows level不能出现 UI profile 与执行权限不一致
环境environments、codex_home、shell overridecwd/workspace 与执行端必须同快照
身份/lineagesource、history mode、fork/parent、originator持久化、遥测与 agent tree 共同使用

original_config_do_not_use 的名字本身就是约束:旧代码仍需要完整 Config,但新逻辑不应绕过已经解析的 configuration 字段回头任意读取它。

5. 同步原语选择 ​

下面的决策图按问题选择原语。它不是 Rust 通用教程,而是 Session 当前字段设计的归纳。

5.1 Tokio ​

state 与 active_turn 会在 async 方法中访问,所以使用 Tokio mutex。但正确模式仍是尽快 clone/提取 值后释放;持锁执行 provider、filesystem、MCP 或 process I/O 会把不相关 Turn 操作串行化。

5.2 std Mutex ​

mcp_prewarm_task 的 std mutex 只包住 Option<JoinHandle> 的 set/take,不跨 await。worker stop 先 take handle、释放锁,再 await JoinHandle;这样不会阻塞 runtime worker 或触发 async mutex 需求。

5.3 Semaphore ​

managed network proxy refresh 需要“整个 rebuild/apply cycle 同时只有一个”,临界区可能跨多个 await。 它不等同于某份数据的 owner,因此使用一个 permit semaphore,而不是把过程状态塞进 SessionState。

5.4 OnceLock与Atomic ​

multi-agent version 一旦选择就固定;MCP reviewer/lifecycle handle 也只安装一次。内部自动 Turn ID 不需要 拿 state 锁,使用 AtomicU64 独立递增。

源码位置:codex-rs/core/src/session/mod.rs :: set_multi_agent_version_if_unset, next_internal_sub_id

rust
pub(crate) fn set_multi_agent_version_if_unset(
    &self,
    multi_agent_version: MultiAgentVersion,
) -> MultiAgentVersion {
    // 并发调用只会让一个值成为Session终身选择,其余读取相同结果。
    *self.multi_agent_version.get_or_init(|| multi_agent_version)
}

fn next_internal_sub_id(&self) -> String {
    let id = self
        .next_internal_sub_id
        .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
    format!("auto-compact-{id}")
}

内部 ID 使用可读前缀,不与用户 Turn 的 UUIDv7 混淆。SeqCst 保证并发生成时形成单一全序。

6. 快照后执行 ​

配置更新和网络 refresh 的典型顺序是:短锁内生成/替换 snapshot,释放锁后通知 contributor 或执行 外部刷新。下面的时序图展示权限变化如何跨越两种锁。

如果把 proxy I/O 放进第一次 state lock,event、Turn 创建和其他 snapshot getter 都会等待外部网络过程。 如果完全不使用 refresh semaphore,两次权限更新又可能交错 publish 旧 proxy。

源码中的 getter 也采用相同原则:

源码位置:codex-rs/core/src/session/mod.rs :: preview_settings, thread_config_snapshot, thread_environment_selections

rust
pub(crate) async fn preview_settings(
    &self,
    updates: &SessionSettingsUpdate,
) -> ConstraintResult<ThreadConfigSnapshot> {
    let state = self.state.lock().await;
    // candidate只在临时clone上apply,不修改当前SessionConfiguration。
    state
        .session_configuration
        .apply(updates)
        .map(|configuration| configuration.thread_config_snapshot())
}

pub(crate) async fn thread_config_snapshot(&self) -> ThreadConfigSnapshot {
    let state = self.state.lock().await;
    state.session_configuration.thread_config_snapshot()
}

pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
    let state = self.state.lock().await;
    state
        .session_configuration
        .environment_selections()
        .to_vec()
}

返回 owned snapshot/Vec 后,调用方不会把 mutex guard 带出 Session。get_config() 同样 clone Arc,而不 暴露对内部字段的可变引用。

7. ActiveTurn双层 ​

ActiveTurn 把 task owner 与当前 Turn waiter 分开。中断需要取得 RunningTask 的 cancellation token; 审批响应只需要访问 TurnState,不应修改 task handle。

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

rust
pub(crate) struct ActiveTurn {
    pub(crate) task: Option<RunningTask>,
    // TurnState拥有独立Arc Mutex,waiter处理可以在释放active slot锁后继续。
    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>,
    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>,
}

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>>,
    pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
    pub(crate) pending_input: TurnInputQueue,
    mailbox_delivery_phase: MailboxDeliveryPhase,
    pub(crate) tool_calls: u64,
    pub(crate) token_usage_at_turn_start: TokenUsage,
    // ... 其余当前Turn授权、MCP metadata与状态标记。
}

Preparing 是为什么不能用 active_turn.is_some() 判断“模型正在运行”。同理,结束 task 后还必须清理 oneshot waiter 和持久化 terminal boundary,最后才能回到 Empty。

8. MCP refresh ​

McpRefresh 是 Session 中一个值得单独看的组合原语:AtomicBool 记录是否需要刷新,Semaphore 控制同时 只有一个 publisher。调用方可以在不阻塞当前 tool call 的情况下 invalidate。

源码位置:codex-rs/core/src/session/mcp_refresh.rs :: McpRefresh, McpRefreshInvalidationGuard

rust
pub(super) struct McpRefresh {
    pending: AtomicBool,
    gate: Semaphore,
}

impl McpRefresh {
    pub(super) fn invalidate(&self) {
        self.pending.store(true, Ordering::Release);
    }

    pub(super) fn claim(&self) -> bool {
        // publisher原子取得一次dirty工作;新invalidation仍可在执行期间再次置true。
        self.pending.swap(false, Ordering::AcqRel)
    }

    pub(super) async fn acquire(&self) -> Result<SemaphorePermit<'_>, AcquireError> {
        self.gate.acquire().await
    }
}

impl Drop for McpRefreshInvalidationGuard<'_> {
    fn drop(&mut self) {
        if !self.published {
            // refresh在publish前被取消时恢复dirty bit,避免更新永久丢失。
            self.refresh.invalidate();
        }
    }
}

prewarm worker 则是 best-effort 优化:容量 1 的 request channel 合并刷新,auth watch 或 request 唤醒 worker,真正执行前仍调用 refresh_mcp_if_dirty()。stop 时先 cancel token,再从 std mutex take handle, 最后 await worker。

9. 字段修改检查 ​

想新增的事实优先位置需要回答的问题
跨 Turn、可恢复的状态SessionState 或 rollout/store是否需要随 resume 重建?
当前 Turn waiter/计数TurnStateterminal 时如何清理?
当前 task owner/取消RunningTaskgraceful 与强制取消各由谁处理?
Thread 级依赖SessionServices是构造固定、ArcSwap 热替换还是局部 mutex?
最多一次的版本/handleOnceLock并发初始化冲突时选哪个值?
可合并 refresh 信号atomic + gate/channel取消时如何恢复 dirty?
单调内部编号atomicordering 与可读前缀是什么?
跨 await 的重建过程semaphore是否应该与数据 mutex 分离?

新增字段后还要检查四类路径:Session 构造与测试 helper 是否初始化、resume/fork 是否恢复或明确不恢复、 shutdown 是否释放后台资源、snapshot/API 是否应该暴露。只让 struct 编译通过,通常不足以维持 Session 的不变量。

10. 字段设计验证 ​

结构定义只能说明“现在有哪些字段”,测试才说明维护者不允许哪些行为退化。下面四组测试分别覆盖 SessionState 的组合更新、TurnState 的归属、dirty refresh 的取消补偿,以及 service facade 的对象身份。

10.1 替换history ​

auto_compact_window 和 history 虽然是 SessionState 中的两个字段,却共同描述同一上下文窗口。测试先写入 估算 prefill,再整体替换 history,最后要求 prefill 回到 None:这证明 replace_history() 不是普通 setter, 而是跨字段不变量的维护入口。

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

rust
// :: replace_history_clears_auto_compact_window_prefill(完整测试)
#[tokio::test]
async fn replace_history_clears_auto_compact_window_prefill() {
    let session_configuration = make_session_configuration_for_tests().await;
    let mut state = SessionState::new(session_configuration);

    // 先模拟当前窗口已经记录过服务端之外的估算输入量。
    state.set_auto_compact_window_estimated_prefill(/*tokens*/ 100);
    // history换代后,旧窗口的prefill不能继续参与新上下文计数。
    state.replace_history(Vec::new(), /*reference_context_item*/ None);

    assert_eq!(
        state.auto_compact_window_snapshot(),
        AutoCompactWindowSnapshot {
            prefill_input_tokens: None,
        }
    );
}

如果调用方直接写 state.history,就可能忘记清理窗口状态。将组合修改封装进 SessionState 方法,是为了让 不变量与 owner 放在同一处,而不只是为了缩短调用代码。

10.2 延迟权限响应 ​

审批请求发出后,当前 Turn 可能已经被替换。record_granted_request_permissions_for_turn_uses_originating_turn 人工创建两个 ActiveTurn,保留第一个的 Arc<Mutex<TurnState>>,再让 Session 指向第二个。响应到达时只允许 修改第一个对象,不能根据“当前 active”误写到新 Turn。

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

rust
// :: record_granted_request_permissions_for_turn_uses_originating_turn(关键断言)
let originating_active_turn = ActiveTurn::default();
let originating_turn_state = Arc::clone(&originating_active_turn.turn_state);
*session.active_turn.lock().await = Some(originating_active_turn);

// 用新ActiveTurn替换权威槽位,模拟旧请求返回前Turn已经迁移。
let current_active_turn = ActiveTurn::default();
let current_turn_state = Arc::clone(&current_active_turn.turn_state);
*session.active_turn.lock().await = Some(current_active_turn);

let requested_permissions = RequestPermissionProfile {
    network: Some(codex_protocol::models::NetworkPermissions {
        enabled: Some(true),
    }),
    ..RequestPermissionProfile::default()
};
session
    .record_granted_request_permissions_for_turn(
        &codex_protocol::request_permissions::RequestPermissionsResponse {
            permissions: requested_permissions.clone(),
            scope: PermissionGrantScope::Turn,
            strict_auto_review: false,
        },
        codex_exec_server::LOCAL_ENVIRONMENT_ID,
        Some(&originating_turn_state), // 说明:响应携带原始对象身份。
    )
    .await;

assert_eq!(
    originating_turn_state
        .lock()
        .await
        .granted_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID),
    Some(requested_permissions.into())
);
assert_eq!(
    current_turn_state
        .lock()
        .await
        .granted_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID),
    None
);
assert_eq!(
    session
        .granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
        .await,
    None
);

这里不能只传 turn_id 后再查当前槽位:TurnState 是内存态 waiter owner,Arc 本身就是请求与原始 owner 之间的能力引用。测试同时证明 Turn scope 权限不会泄漏到 Session scope。

10.3 dirty ​

cancelled_mcp_refresh_remains_pending 故意持有 SessionState 锁,让 refresh future 在已经 claim() dirty bit、但尚未完成 desired-state snapshot 时进入 Pending。随后直接 drop future,等价于任务在 publish 前取消。

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

rust
// :: cancelled_mcp_refresh_remains_pending(完整核心路径)
{
    let _state = session.state.lock().await;
    {
        let mut refresh = Box::pin(session.refresh_mcp_if_dirty());
        let mut context =
            std::task::Context::from_waker(futures::task::noop_waker_ref());

        // refresh已消费dirty bit,随后阻塞在SessionState snapshot。
        assert!(std::future::Future::poll(refresh.as_mut(), &mut context).is_pending());
        assert!(!session.mcp_refresh.is_pending());
    } // 说明:future在publish前drop,InvalidationGuard随之恢复dirty bit。
}

assert!(session.mcp_refresh.is_pending());
session.refresh_mcp_if_dirty().await;
assert!(!session.mcp_refresh.is_pending());

这组断言覆盖了最危险的窗口:pending.swap(false) 已经发生,但新 runtime 尚未发布。如果没有 McpRefreshInvalidationGuard::drop(),下一次调用会看到 clean 并直接返回,配置变化便永久丢失。

10.4 runtime刷新 ​

MCP runtime 持有 elicitation reviewer。刷新后若换成一个新的 Arc,已经保存旧引用的 MCP connection 仍会调用过期对象。测试用 Arc::ptr_eq 检查的是同一 allocation,而不是两个 reviewer 的字段值相等。

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

rust
// :: mcp_elicitation_reviewer_is_reused_across_runtime_refreshes(完整测试)
#[tokio::test]
async fn mcp_elicitation_reviewer_is_reused_across_runtime_refreshes() {
    let (session, _turn_context) = make_session_and_context().await;
    let session = Arc::new(session);
    let previous = session.mcp_elicitation_reviewer();

    session.mark_mcp_runtime_dirty();
    session.refresh_mcp_if_dirty().await;

    // refresh更新reviewer内部authority,外部持有的facade身份保持稳定。
    assert!(Arc::ptr_eq(&previous, &session.mcp_elicitation_reviewer()));
}

相同的身份判断也出现在 task 收尾:只有当槽位中的 turn_state 仍与完成任务持有的 turn_state 指向同一 对象时,旧任务才可清空 active_turn。

源码位置:codex-rs/core/src/tasks/mod.rs :: Session::on_task_finished(节选)

rust
let cleared_active_turn = {
    let mut active = self.active_turn.lock().await;
    if let Some(active_turn) = active.as_ref()
        && active_turn.task.is_none()
        // 防止迟到的旧task把已经替换的新ActiveTurn清空。
        && Arc::ptr_eq(&active_turn.turn_state, &turn_state)
    {
        *active = None;
        true
    } else {
        false
    }
};

当前测试覆盖 task 正常完成后清空 active_turn,但没有把“旧 task 延迟收尾、新 ActiveTurn 已占位, 新槽位不被清空”的竞态组合成直接断言。因此,Arc::ptr_eq 的保护意图有源码依据,完整竞态仍应视为未覆盖 场景,而不是已经由测试完整确认。

11. 状态同步验证 ​

读完后,可以不用运行整个工作区,先通过下面的只读命令验证三条主线:

bash
# 1. SessionState、ActiveTurn与SessionServices分别在哪里定义?
rg -n "struct (SessionState|ActiveTurn|SessionServices)" codex-rs/core/src

# 2. 哪些位置用对象身份阻止旧Turn清理或覆盖新Turn?
rg -n "Arc::ptr_eq.*turn_state|ptr_eq\(&active_turn.turn_state" \
  codex-rs/core/src/session codex-rs/core/src/tasks

# 3. dirty bit从占用到发布之间由谁提供取消补偿?
rg -n "McpRefreshInvalidationGuard|refresh_invalidation.published" \
  codex-rs/core/src/session

如果能解释下面三个问题,就已经掌握本文重点:

  1. 为什么 history 被替换时,auto-compaction window 也必须由同一个方法更新?
  2. 为什么延迟返回的 Turn 权限响应不能通过 session.active_turn 重新寻找 owner?
  3. 为什么 Arc::ptr_eq 不能替换为值相等比较?

下一步可阅读 Session依赖装配,观察这些容器如何获得初值; 再阅读 Session关闭流程,追踪 task、worker 和 service owner 如何按相反顺序释放。