雨天小六

读懂 Codex(3.4):SessionServices、SessionState 与可变状态的分界

· 更新于 2026-08-02 · 专栏:读懂 Codex

#Codex#Agent Runtime#生命周期#软件架构

Session 内部并不是“一个大锁保护所有东西”。源码把长期服务句柄、跨 Turn 值状态和当前 Turn 状态拆成三层;这个拆分决定哪些对象可以无锁共享、哪些更新必须原子发生、哪些等待者在 Turn 结束时必须被清空。

三层所有权

主要类型生命周期典型内容
长期服务SessionServicesSession 全程模型、MCP、Shell、Rollout、Plugins、Network Proxy、AgentControl
跨 Turn 状态SessionStateSession 全程、持续修改SessionConfiguration、历史、Token、限流、连接器选择
当前 Turn 状态ActiveTurn / TurnState一次活动 TurnRunningTask、审批 waiter、pending input、临时权限、工具计数
SessionServices、SessionState 和 ActiveTurn 三层状态所有权
图 3.4-1:Services 保存可调用的长期能力,SessionState 保存跨 Turn 的值,ActiveTurn 保存只能属于当前工作的等待者和统计。

SessionServices 里的对象通常已经通过 Arc、ArcSwap 或内部锁实现并发控制,因此外层不再用 SessionState 的互斥锁包住网络调用。例如 ModelClient、MCP runtime、UnifiedExecProcessManager 和 Rollout recorder 都有自己的生命周期。

SessionState 则把需要一致观察的一组跨 Turn 值放在同一个 Tokio Mutex 后面。最典型的是“应用本次 settings update,同时生成 TurnContext”:若模型已经换成 B,而权限仍从旧配置 A 读取,新的 Turn 会得到撕裂快照。单锁让配置变换和快照来源形成一个原子事务。

class Session:
    services: SessionServices                 # 长期句柄
    state: Mutex[SessionState]                # 跨 Turn 值
    active_turn: Mutex[Optional[ActiveTurn]]  # 当前 Turn 容器
    input_queue: InputQueue                    # Session + Turn 两级输入

async def begin_turn(update):
    async with state.lock() as s:
        next_config = apply_update(s.session_configuration, update)
        s.session_configuration = next_config
        snapshot_source = next_config.clone()
    return await build_turn_context(snapshot_source)

SessionState 为什么不装服务对象

SessionState 包含 ContextManager、Token/Ratelimit、额外上下文、PreviousTurnSettings、自动压缩窗口、启动预热、时间提醒、连接器选择、环境选择配置和已授予权限等。这些都是“当前会话事实”,恢复、换 Turn 或配置更新会改变。

如果把网络服务也塞进同一个 Mutex,读取历史时可能持锁等待 MCP 或 HTTP,Submission 主循环随之无法响应 Interrupt。相反,如果把 SessionConfiguration 拆成许多独立锁,本次 Turn 又可能组合出不同时间点的模型、权限和环境选择。当前边界是在一致性和并发性之间做的折中。

ActiveTurn 再拆执行对象和领域状态

ActiveTurn 同时保存可选 RunningTask 与共享 TurnState。RunningTask 是执行壳:Task 策略、CancellationToken、AbortOnDropHandle、TurnContext、Notify 和执行计时器。TurnState 则保存工具执行过程中会被 Submission handler 回填的 oneshot sender、pending input、MCP 审批元数据和临时权限。

ActiveTurn 内 RunningTask 与 TurnState 的并发协作
图 3.4-2:RunningTask 决定代码是否仍在运行,TurnState 让审批答复和 Steer 能从 Submission loop 回到那次 Turn;两者的清理顺序不能互换。
class ActiveTurn:
    task: Optional[RunningTask]
    turn_state: Arc[Mutex[TurnState]]

class TurnState:
    pending_input: list[TurnInput]
    approval_waiters: dict[CallId, OneShotSender]
    permission_waiters: dict[CallId, OneShotSender]
    dynamic_tool_waiters: dict[CallId, OneShotSender]
    granted_permissions: set[Permission]
    tool_calls: int

Task 正常返回时,完成处理先从 ActiveTurn 取走 task,但仍保留 TurnState 来统计和处理边界输入;发终态后才用 Arc 指针身份确认并清空整个 ActiveTurn。取消时则先拿走 ActiveTurn,阻断新输入,再通知任务退出,最后清 pending waiters。这个顺序体现了“执行所有权”和“领域状态所有权”是两个阶段。

锁边界的规则

  1. 不在 SessionState 锁内做长网络 I/O;先复制配置,再在锁外刷新服务。
  2. ActiveTurn 与 TurnState 需要联合判断时保持固定加锁顺序,避免 steer 与完成竞态。
  3. 跨 Turn 的授权不能误放进 TurnState;仅本轮批准则不能写入永久配置。
  4. Features 在 Session 建立后视为不变量,避免每个热路径反复加锁。

评论


← 返回文章列表