English 书架

第 2 部

构建运行时

运行时是 context、streaming、tools、cancellation 和 replay 的调度器。

线程、会话与持久状态

阅读契约: 把本章当作 Codex 持久状态地图。重点区分 thread id、live session、queue-pair facade、模型可见 history、rollout replay facts 和 query projection。读完后,你应该能判断:resume、fork、rollback 或启动新 turn 时,到底是哪一个 owner 必须改变。

Codex live runtime handles mapped to durable thread records, rollout, and state database
Codex thread 不是一个聊天对象。live handle 服务当前进程;durable record 让工作在进程变化后仍可恢复。

源码边界: 本章直接源码判断固定在 OpenAI Codex commit 569ff6a1c400bd514ff79f5f1050a684dc3afde3ThreadStoreLiveThreadThreadManagerCodexSessionContextManagerRolloutItemInitialHistoryTurnContextTurnContextItem 只有在正文链接到固定源码时才作为 verified source。 “durable ledger”“runtime projection”“execution envelope”“history surface” 这些说法是从可见源码形状得出的 surrounding contract inference,不是对 OpenAI 私有服务内部的断言。

先从协议边界开始。客户端可以提交 operation,并接收带有关联 id 的事件; 但这条窄接口本身解释不了:一段对话如何跨进程恢复,fork 如何选择前缀, rollback 为什么不是删除屏幕上的几张卡片。

持久状态问题比 transcript 问题更难。一个 coding agent 同时有 in-flight live work、pending user input、tool approval、模型可见 history、UI event、 rollout file、thread metadata、search index,以及每个 turn 的执行设置。 如果把这些全部塞进一个 messages 列表,resume 会丢语义,fork 会变得含混, rollback 会变得危险。

Codex 的答案是拆 owner:

问题:一个用户可见 thread 必须能 resume、fork、interrupt 和 query,同时 live task 仍然通过窄事件协议运行。

主张:Codex 把 durable thread identity 与 live session 分开,再从可 replay 的 rollout facts 重建模型可见 history。

心智模型:thread 是长期工作账本;session 是当前服务它的活进程;turn 是 session 内被调度的一段工作。

导读问题:这个事实由哪个 owner 回答?哪个 surface 可 replay?哪个 state 只是 live handle?

一、Thread 是持久边界

1.1 ThreadStore 是存储无关契约

第一个关键源码形状不是 Session,而是 ThreadStore。 这个 trait 描述 durable thread storage 必须提供什么:打开 live persistence, 追加有序事实,让排队写入变得可读,处理初始化失败时的 discard,并为 resume/fork/rollback 加载历史。下面是节选;trait 后半部分的 read/list/update 接口在这里省略。

/// Storage-neutral thread persistence boundary.
#[async_trait]
pub trait ThreadStore: Any + Send + Sync {
    /// Return this store as [`Any`] for implementation-owned escape hatches.
    fn as_any(&self) -> &dyn Any;

    /// Creates a new live thread.
    async fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreResult<()>;

    /// Reopens an existing thread for live appends.
    async fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreResult<()>;

    /// Appends items to a live thread.
    async fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreResult<()>;

    /// Materializes the thread if persistence is lazy, then persists all queued items.
    async fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>;

    /// Flushes all queued items and returns once they are durable/readable.
    async fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>;

    /// Flushes pending items and closes the live thread writer.
    async fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>;

    /// Discards the live thread writer without forcing pending in-memory items to become durable.
    async fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()>;

    /// Loads persisted history for resume, fork, rollback, and memory jobs.
    async fn load_history(
        &self,
        params: LoadThreadHistoryParams,
    ) -> ThreadStoreResult<StoredThreadHistory>;

    // ...
}

这个 trait 是 durable contract。live process 不需要知道背后是本地 rollout 文件、state database,还是远端服务。可见 invariant 更简单:live thread 一旦打开,后续 runtime facts 就可以按顺序 append、flush,并在之后 replay。

参数也把这层拆分写得很清楚。 thread-store/src/types.rs 里,new 和 resumed persistence 都携带 thread-scoped metadata,包括有效 working directory、model provider 和 memory mode。resumed thread 还可以带 已加载的 rollout history 与 rollout path。下面节选省略了夹在中间的 CreateThreadParams

pub struct ThreadPersistenceMetadata {
    pub cwd: Option<PathBuf>,
    pub model_provider: String,
    pub memory_mode: MemoryMode,
}

// ...

pub struct ResumeThreadParams {
    pub thread_id: ThreadId,
    pub rollout_path: Option<PathBuf>,
    pub history: Option<Vec<RolloutItem>>,
    pub include_archived: bool,
    pub metadata: ThreadPersistenceMetadata,
    pub event_persistence_mode: ThreadEventPersistenceMode,
}

所以 thread id 不只是 UI key。它是未来写入和 replay 读取共同指向的 durable handle。

1.2 LiveThread 让 session 保持窄边界

Codex 用 LiveThread 包装 store。源码注释说得很直白:session code 只应该需要 active thread 的 live handle,storage details 留在 ThreadStore 里面。

/// Handle for an active thread's persistence lifecycle.
///
/// `LiveThread` keeps lifecycle decisions with the caller while delegating storage details to
/// [`ThreadStore`]. Local stores may use a rollout file internally and remote stores may use a
/// service, but session code should only need this handle for the active thread.
#[derive(Clone)]
pub struct LiveThread {
    thread_id: ThreadId,
    thread_store: Arc<dyn ThreadStore>,
}

这个 wrapper 暴露 running session 需要的动词: createresumeappend_itemspersistflush, 以及 shutdown / discard。初始化失败还有单独的 LiveThreadInitGuard: 如果 persistence 已经打开,但 session 构建失败,guard 会 discard live writer。

这里的实际规则很硬:打开 live writer 不等于 session 已经成功暴露。直到 session 真正接管 handle 之前,初始化都必须可回滚。

二、运行时栈是一组 handle

2.1 ThreadManager 拥有 live threads

下一层是 ThreadManager。 它持有 ThreadIdCodexThread 的 map,以及 auth、models、environments、 skills、plugins、MCP、extensions、thread store、attestation、analytics 和 可选 state DB 等共享服务。

/// [`ThreadManager`] is responsible for creating threads and maintaining
/// them in memory.
pub struct ThreadManager {
    state: Arc<ThreadManagerState>,
    _test_codex_home_guard: Option<TempCodexHomeGuard>,
}

// ...

pub(crate) struct ThreadManagerState {
    threads: Arc<RwLock<HashMap<ThreadId, Arc<CodexThread>>>>,
    thread_created_tx: broadcast::Sender<ThreadId>,
    auth_manager: Arc<AuthManager>,
    models_manager: SharedModelsManager,
    environment_manager: Arc<EnvironmentManager>,
    skills_manager: Arc<SkillsManager>,
    plugins_manager: Arc<PluginsManager>,
    mcp_manager: Arc<McpManager>,
    extensions: Arc<ExtensionRegistry<Config>>,
    thread_store: Arc<dyn ThreadStore>,
    state_db: Option<StateDbHandle>,
    // ...
}

这个 field list 的意义是 live ownership 集中在一个地方。客户端不直接改 Session,而是请求 manager start、resume、fork 或 retrieve thread; manager 决定是否已有 running handle,以及是否需要 spawn 新 session。它保护的 invariant 是:一个 durable future 不应该同时有两个 live writer。

对于 resumed thread,这个 invariant 在 Session 暴露之前就开始执行。 spawn_thread 里,如果 resumed thread 已经 running 且 rollout path 匹配,manager 直接返回 现有 thread;如果 map 里是 stopped entry,则先移除再 spawn 新 Codex session。这样可以避免两个 live writer 给同一个 durable record 追加不同未来。

2.2 Codex 只是 queue pair

对外 runtime handle 刻意很小。 session/mod.rsCodex 描述为一个高层接口:send submissions,receive events。

/// The high-level interface to the Codex system.
/// It operates as a queue pair where you send submissions and receive events.
pub struct Codex {
    pub(crate) tx_sub: Sender<Submission>,
    pub(crate) rx_event: Receiver<Event>,
    pub(crate) agent_status: watch::Receiver<AgentStatus>,
    pub(crate) session: Arc<Session>,
    pub(crate) session_loop_termination: SessionLoopTermination,
}

Session 则是大的内部对象。它的定义说明 session 同时最多只有一个 running task,并且可以被 user input interrupt;字段包含 event output、session state、 active turn、mailbox、pending input、goal runtime、review session 和 service handles (session/session.rs)。

/// Context for an initialized model agent
///
/// A session has at most 1 running task at a time, and can be interrupted by user input.
pub(crate) struct Session {
    pub(crate) conversation_id: ThreadId,
    pub(crate) installation_id: String,
    pub(super) tx_event: Sender<Event>,
    pub(super) agent_status: watch::Sender<AgentStatus>,
    pub(super) out_of_band_elicitation_paused: watch::Sender<bool>,
    pub(super) state: Mutex<SessionState>,
    pub(super) managed_network_proxy_refresh_lock: Semaphore,
    pub(super) features: ManagedFeatures,
    pub(super) pending_mcp_server_refresh_config: Mutex<Option<McpServerRefreshConfig>>,
    pub(crate) conversation: Arc<RealtimeConversationManager>,
    pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
    pub(super) mailbox: Mailbox,
    pub(super) mailbox_rx: Mutex<MailboxReceiver>,
    pub(super) idle_pending_input: Mutex<Vec<ResponseInputItem>>,
    pub(crate) goal_runtime: GoalRuntimeState,
    pub(crate) guardian_review_session: GuardianReviewSessionManager,
    pub(crate) services: SessionServices,
    pub(super) next_internal_sub_id: AtomicU64,
}

把运行时栈压成一句话:ThreadManager 管 live map,CodexThread 是稳定外部 handle,Codex 是 submission/event queue pair,Session 是 live scheduler, ThreadStore 是背后的持久边界。

三、启动顺序有第一个事件 invariant

Codex session startup timeline where configuration, persistence, and session construction emit SessionConfigured before later history, MCP, and prewarm work
SessionConfigured 是事件流锚点。后续启动工作可以继续,但客户端先拿到已解析的 thread/session envelope。

3.1 SessionConfigured 打开事件流

Codex::spawn 返回后,manager 不会立刻发布新 thread。它先读取下一个 event, 并要求它是 SessionConfiguredfinalize_thread_spawn 里,任何其他 event 都是错误。

async fn finalize_thread_spawn(
    &self,
    codex: Codex,
    thread_id: ThreadId,
    session_source: SessionSource,
) -> CodexResult<NewThread> {
    let event = codex.next_event().await?;
    let session_configured = match event {
        Event {
            id,
            msg: EventMsg::SessionConfigured(session_configured),
        } if id == INITIAL_SUBMIT_ID => session_configured,
        _ => {
            return Err(CodexErr::SessionConfiguredNotFirstEvent);
        }
    };

    let mut threads = self.threads.write().await;
    if let std::collections::hash_map::Entry::Vacant(e) = threads.entry(thread_id) {
        let thread = Arc::new(CodexThread::new(
            codex,
            session_configured.clone(),
            session_configured.rollout_path.clone(),
            session_source,
        ));
        e.insert(thread.clone());
        return Ok(NewThread { thread_id, thread, session_configured });
    }

    Err(CodexErr::InvalidRequest(format!("thread {thread_id} is already running")))
}

这个 invariant 不是 UI 美学。如果客户端在知道 thread id、session id、model、 provider、permission profile、cwd、initial messages 和 rollout path 之前就收到 后续事件,它只能猜这些事实属于哪一个 durable thread。Codex 把 setup event 固定成 stream anchor。

3.2 可选启动工作在 anchor 之后继续

session 会先发送 setup event,再报告其他启动事实。 session/session.rs 里,第一个 event 包含 session_idthread_idforked_from_id、source classification、model、provider、service tier、approval policy、permission profile、cwd、reasoning effort、initial messages、network proxy 和 rollout path。 之后才把 post-setup events 接到同一条 stream 上。

// Dispatch the SessionConfiguredEvent first and then report any errors.
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
let initial_messages = initial_history.get_event_msgs();
let events = std::iter::once(Event {
    id: INITIAL_SUBMIT_ID.to_owned(),
    msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
        session_id,
        thread_id,
        forked_from_id,
        thread_source: session_configuration.thread_source,
        thread_name: session_configuration.thread_name.clone(),
        model: session_configuration.collaboration_mode.model().to_string(),
        model_provider_id: config.model_provider_id.clone(),
        service_tier: session_configuration.service_tier.clone(),
        approval_policy: session_configuration.approval_policy.value(),
        approvals_reviewer: session_configuration.approvals_reviewer,
        permission_profile: session_configuration.permission_profile(),
        active_permission_profile: session_configuration.active_permission_profile(),
        cwd: session_configuration.cwd.clone(),
        reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
        initial_messages,
        network_proxy: session_network_proxy.filter(|_| {
            Self::managed_network_proxy_active_for_permission_profile(
                session_configuration.permission_profile.get(),
            )
        }),
        rollout_path,
    }),
})
.chain(post_session_configured_events.into_iter());
for event in events {
    sess.send_event_raw(event).await;
}

MCP initialization、startup prewarm 和 initial-history recording 之后继续 (session/session.rs)。 于是 resumed thread 可以先渲染 identity 和 initial messages,不必等每个可选 runtime capability 都完成加载。

四、History 有三个 owner

One Codex runtime item fans out to ContextManager model view, rollout JSONL replay facts, State DB query projection, and client events
一个 runtime fact 可以同时影响 model view、replay ledger、query projection 和 client stream,但它们回答的问题不同。

4.1 ContextManager 拥有模型可见视图

第一个 history owner 是 ContextManager。 它保存 ResponseItem,跟踪 history_version,携带 token usage,并记住未来 settings diff 使用的 reference_context_item

/// Transcript of thread history
#[derive(Debug, Clone, Default)]
pub(crate) struct ContextManager {
    /// The oldest items are at the beginning of the vector.
    items: Vec<ResponseItem>,
    /// Bumped whenever history is rewritten, such as compaction or rollback.
    history_version: u64,
    token_info: Option<TokenUsageInfo>,
    /// Reference context snapshot used for diffing and producing model-visible
    /// settings update items.
    reference_context_item: Option<TurnContextItem>,
}

for_prompt 这个方法名就是关键。它返回准备发送给模型的 history,而且会先做 normalization 与 modality filtering。这不是 UI transcript,也不是 durable JSONL record。

/// Returns the history prepared for sending to the model. This applies a proper
/// normalization and drops un-suited items. When `input_modalities` does not
/// include `InputModality::Image`, images are stripped from messages and tool
/// outputs.
pub(crate) fn for_prompt(mut self, input_modalities: &[InputModality]) -> Vec<ResponseItem> {
    self.normalize_history(input_modalities);
    self.items
}

如果 rollback 或 compaction 改写 history,ContextManager 是必须改变的 model-side surface。UI 可以继续显示发生过什么,durable record 可以保留 replay facts,但下一次模型请求必须看到重建后的视图。

4.2 RolloutItem 拥有 replay facts

第二个 owner 是 rollout vocabulary。 protocol.rs 里,RolloutItem 有五类:session metadata、response items、compacted items、 turn context snapshots 和 event messages。

#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
pub enum RolloutItem {
    SessionMeta(SessionMetaLine),
    ResponseItem(ResponseItem),
    Compacted(CompactedItem),
    TurnContext(TurnContextItem),
    EventMsg(EventMsg),
}

本地 writer 把存储格式说得很清楚。RolloutRecorder 说明 rollouts 被记录为 JSONL,它的命令通道接收 AddItemsPersistFlushShutdown (rollout/src/recorder.rs)。

/// Rollouts are recorded as JSONL and can be inspected with tools such as:
///
/// ```ignore
/// $ jq -C . ~/.codex/sessions/rollout-2025-05-07T17-24-21-5973b6c0-94b8-487b-a530-2aeb6098ae0e.jsonl
/// ```
#[derive(Clone)]
pub struct RolloutRecorder {
    tx: Sender<RolloutCmd>,
    writer_task: Arc<RolloutWriterTask>,
    pub(crate) rollout_path: PathBuf,
    event_persistence_mode: EventPersistenceMode,
}

所以 durable record 不是“模型看到过什么”。它是 replay ledger,可以包含 session metadata、event records、context snapshots 和 compaction facts,其中很多并不是 直接模型输入。

4.3 State DB 拥有 query projections

第三个 owner 是 state database。Local storage 把 StateDbHandle 挂进 LocalThreadStore;state runtime 打开 SQLite-backed handle,按需做 rollout metadata backfill,并返回初始化后的 handle (rollout/src/state_db.rs)。

#[derive(Clone)]
pub struct LocalThreadStore {
    pub(super) config: LocalThreadStoreConfig,
    live_recorders: Arc<Mutex<HashMap<ThreadId, RolloutRecorder>>>,
    state_db: Option<StateDbHandle>,
}

Metadata 是从 replayable facts 抽出来的,不是独立编造的。metadata extractor 会寻找 RolloutItem::SessionMeta 并从它构造 thread metadata;如果没有,就回退到 从文件名推导的 legacy metadata (rollout/src/metadata.rs)。

pub fn builder_from_items(
    items: &[RolloutItem],
    rollout_path: &Path,
) -> Option<ThreadMetadataBuilder> {
    if let Some(session_meta) = items.iter().find_map(|item| match item {
        RolloutItem::SessionMeta(meta_line) => Some(meta_line),
        RolloutItem::ResponseItem(_)
        | RolloutItem::Compacted(_)
        | RolloutItem::TurnContext(_)
        | RolloutItem::EventMsg(_) => None,
    }) && let Some(builder) = builder_from_session_meta(session_meta, rollout_path)
    {
        return Some(builder);
    }

    // Legacy fallback derives thread metadata from the rollout filename.
    // ...
}

三种 history 的规则可以压成这张表:

SurfaceOwner主要问题如果混成一个东西
模型可见视图ContextManager下一次模型请求应该看到什么?resume 可能把 stale、compacted 或 rolled-back item 泄进 inference。
Replay ledgerRolloutItem JSONL / storedurable order 里发生过什么?fork 和 rollback 缺少重建事实。
Query projectionState DB / metadata builders什么信息要高效 list/search/index?thread list 要么全量 replay,要么 drift。
Client streamEvent / EventMsg客户端现在应该渲染什么?UI timing 变成事实来源,替代 durable replay。

五、Resume 与 fork 是 replay 选择

Codex resume and fork reconstruct history from rollout scan, compaction, rollback, surviving prefix, and replacement history
Resume 和 fork 不是复制 transcript。它们选择 replay span,应用 compaction 与 rollback 语义,再安装重建后的模型可见 history。

5.1 InitialHistory 命名启动模式

协议可见的启动模式是 InitialHistory: new、cleared、带 conversation id/history/rollout path 的 resumed,以及来自 rollout items 的 forked。

pub struct ResumedHistory {
    pub conversation_id: ThreadId,
    pub history: Vec<RolloutItem>,
    pub rollout_path: Option<PathBuf>,
}

pub enum InitialHistory {
    New,
    Cleared,
    Resumed(ResumedHistory),
    Forked(Vec<RolloutItem>),
}

session 会按不同模式记录 initial history。 record_initial_history 里,new 与 cleared session 会把 initial context insertion 延迟到第一个真实 turn; resumed session 会 reconstruct history、seed token usage,并在需要时 flush rollout; forked session 会 reconstruct history、seed token usage、persist forked rollout items、materialize rollout,再 flush。

match conversation_history {
    InitialHistory::New | InitialHistory::Cleared => {
        // Defer initial context insertion until the first real turn starts.
        self.set_previous_turn_settings(/*previous_turn_settings*/ None).await;
    }
    InitialHistory::Resumed(resumed_history) => {
        let rollout_items = resumed_history.history;
        let previous_turn_settings = self
            .apply_rollout_reconstruction(&turn_context, &rollout_items)
            .await;

        if let Some(info) = Self::last_token_info_from_rollout(&rollout_items) {
            let mut state = self.state.lock().await;
            state.set_token_info(Some(info));
        }

        if !is_subagent {
            let _ = self.flush_rollout().await;
        }
    }
    InitialHistory::Forked(rollout_items) => {
        self.apply_rollout_reconstruction(&turn_context, &rollout_items).await;
        if !rollout_items.is_empty() {
            self.persist_rollout_items(&rollout_items).await;
        }
        self.ensure_rollout_materialized().await;
        if !is_subagent {
            let _ = self.flush_rollout().await;
        }
    }
}

关键区别是:resumed thread 给已有 identity 追加未来写入;fork 则把选定 replay prefix 安装进一个新的未来。两者都依赖同一套 rollout vocabulary。

5.2 Reconstruction 从新到旧扫描

apply_rollout_reconstruction 调用 reconstruct_history_from_rollout, 然后替换 ContextManager history,并恢复 previous turn settings。

async fn apply_rollout_reconstruction(
    &self,
    turn_context: &TurnContext,
    rollout_items: &[RolloutItem],
) -> Option<PreviousTurnSettings> {
    let reconstructed_rollout = self
        .reconstruct_history_from_rollout(turn_context, rollout_items)
        .await;
    let previous_turn_settings = reconstructed_rollout.previous_turn_settings.clone();
    self.replace_history(
        reconstructed_rollout.history,
        reconstructed_rollout.reference_context_item,
    )
    .await;
    self.set_previous_turn_settings(previous_turn_settings.clone()).await;
    previous_turn_settings
}

reconstruction 模块解释了 replay shape。它从 newest 到 oldest 扫描 rollout items, 寻找最新的 surviving replacement-history checkpoint、最新的 surviving turn settings,以及最新的 surviving context baseline (rollout_reconstruction.rs)。

// Replay metadata should already match the shape of the future lazy reverse loader, even
// while history materialization still uses an eager bridge. Scan newest-to-oldest,
// stopping once a surviving replacement-history checkpoint and the required resume metadata
// are both known; then replay only the buffered surviving tail forward to preserve exact
// history semantics.
let mut base_replacement_history: Option<&[ResponseItem]> = None;
let mut previous_turn_settings = None;
let mut reference_context_item = TurnReferenceContextItem::NeverSet;
// Rollback is "drop the newest N user turns". While scanning in reverse, that becomes
// "skip the next N user-turn segments we finalize".
let mut pending_rollback_turns = 0usize;

之后 suffix 被正向 replay 到新的 ContextManager。response item 进入模型 history; replacement compaction 可以替换 base history;ThreadRolledBack event 会 drop user turn;其他 rollout item 不直接 materialize 成模型 history (rollout_reconstruction.rs)。

for item in rollout_suffix {
    match item {
        RolloutItem::ResponseItem(response_item) => {
            history.record_items(
                std::iter::once(response_item),
                turn_context.truncation_policy,
            );
        }
        RolloutItem::Compacted(compacted) => {
            if let Some(replacement_history) = &compacted.replacement_history {
                history.replace(replacement_history.clone());
            } else {
                // Legacy compaction fallback rebuilds compacted history.
                // ...
            }
        }
        RolloutItem::EventMsg(EventMsg::ThreadRolledBack(rollback)) => {
            history.drop_last_n_user_turns(rollback.num_turns);
        }
        RolloutItem::EventMsg(_)
        | RolloutItem::TurnContext(_)
        | RolloutItem::SessionMeta(_) => {}
    }
}

这就是 durable-state 的核心:rollback 不是 UI delete,compaction 不是显示摘要, fork 不是复制 DOM。它们都是对 durable ledger 的 replay 选择,重建结果会成为下一次 模型可见 history。

5.3 Fork truncation 保留 turn 边界

fork 还要决定 prefix 在哪里结束。 thread_rollout_truncation.rs 就是这个边界的 helper:找到 user message positions、应用 rollback markers,并在 选定 user turn 之前 truncate。thread manager 在从 rollout history fork 时使用这套 逻辑 (thread_manager.rs)。

可见契约是有限的。源码展示了 prefix selection 和 replay reconstruction;它并不 需要声称 fork 是每个 UI artifact 的字节级复制。真正 durable 的,是可 replay 的 prefix 以及它之后的新 append path。

六、Turn context 是执行语义信封

TurnContext persists cwd, permissions, model, and network into TurnContextItem as a reference baseline for the next turn
TurnContextItem 是 durable execution envelope。replay 恢复的不只是文本,还包括真实 user turn 周围的执行设置。

6.1 TurnContext 携带 runtime semantics

TurnContext 是让一次模型请求可执行的 per-turn bundle。 turn_context.rs 里,它包含当前 sub id、trace id、realtime state、config、auth、model、provider、 session source、thread source、resolved environments、cwd、date、timezone、 developer/user instructions、collaboration mode、approval policy、permission profile、network proxy、shell policy、tool config、feature state、dynamic tools、 skills context 和 truncation policy。

pub struct TurnContext {
    pub(crate) sub_id: String,
    pub(crate) trace_id: Option<String>,
    pub(crate) realtime_active: bool,
    pub config: Arc<Config>,
    // ...
    pub(crate) model_info: ModelInfo,
    pub(crate) provider: SharedModelProvider,
    // ...
    pub(crate) session_source: SessionSource,
    pub(crate) thread_source: Option<ThreadSource>,
    pub(crate) environments: ResolvedTurnEnvironments,
    /// The session's absolute working directory.
    pub(crate) cwd: AbsolutePathBuf,
    pub(crate) current_date: Option<String>,
    pub(crate) timezone: Option<String>,
    // ...
    pub(crate) developer_instructions: Option<String>,
    pub(crate) user_instructions: Option<String>,
    // ...
    pub(crate) approval_policy: Constrained<AskForApproval>,
    pub(crate) permission_profile: PermissionProfile,
    pub(crate) network: Option<NetworkProxy>,
    // ...
    pub(crate) truncation_policy: TruncationPolicy,
    // ...
}

所以 replay 不能只是“messages”。后续 turn 需要知道相对路径基于哪个 cwd 解析, 当时 sandbox 与 approval 规则是什么,使用了哪个 model,network policy 是否相关, 以及哪些 instructions 或 schema 生效。

6.2 TurnContextItem 持久化 baseline

durable 形式由 to_turn_context_item 生成。

pub(crate) fn to_turn_context_item(&self) -> TurnContextItem {
    TurnContextItem {
        turn_id: Some(self.sub_id.clone()),
        trace_id: self.trace_id.clone(),
        cwd: self.cwd.to_path_buf(),
        current_date: self.current_date.clone(),
        timezone: self.timezone.clone(),
        approval_policy: self.approval_policy.value(),
        sandbox_policy: self.sandbox_policy(),
        permission_profile: Some(self.permission_profile()),
        network: self.turn_context_network_item(),
        file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),
        model: self.model_info.slug.clone(),
        personality: self.personality,
        collaboration_mode: Some(self.collaboration_mode.clone()),
        realtime_active: Some(self.realtime_active),
        effort: self.reasoning_effort,
        summary: self.reasoning_summary,
        user_instructions: self.user_instructions.clone(),
        developer_instructions: self.developer_instructions.clone(),
        final_output_json_schema: self.final_output_json_schema.clone(),
        truncation_policy: Some(self.truncation_policy),
    }
}

protocol struct 用存储语言表达了同一个意思。TurnContextItem 会在每个真实 user turn 计算完模型可见 context updates 后持久化一次;mid-turn compaction 重新建立 full context 时也会再持久化一次 (protocol.rs)。

runtime path 是 record_context_updates_and_set_reference_context_item。 如果没有 baseline,它注入 full initial context;如果已有 baseline,它只生成 settings diffs。然后它追加 RolloutItem::TurnContext,并推进内存里的 reference baseline。

let should_inject_full_context = reference_context_item.is_none();
let context_items = if should_inject_full_context {
    self.build_initial_context(turn_context).await
} else {
    self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
        .await
};
let turn_context_item = turn_context.to_turn_context_item();
if !context_items.is_empty() {
    self.record_conversation_items(turn_context, &context_items).await;
}
// Persist one `TurnContextItem` per real user turn so resume/lazy replay can recover the
// latest durable baseline even when this turn emitted no model-visible context diffs.
self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())])
    .await;

let mut state = self.state.lock().await;
state.set_reference_context_item(Some(turn_context_item));

这就是 execution-envelope invariant:模型可见 history 可以重建,但重建后的 turn 不应该丢掉原本让那次工作有意义的执行设置。

七、每个 owner 允许忘掉什么

持久系统不是把所有东西存到所有地方,而是明确哪个 owner 对哪个问题权威。

压力简单但错误的设计Codex 的 owner 拆分保护的 invariant
客户端重连从当前屏幕状态重渲染SessionConfigured + event stream后续事件之前,客户端先锚定 thread/session identity。
进程 resume thread加载 messages arrayInitialHistory::Resumed + rollout reconstruction模型可见 history 从 replay facts 重建。
用户 fork 工作复制 UI transcriptprefix selection + InitialHistory::Forked新 thread future 从一致 replay prefix 分叉。
rollback 移除工作删除最新可见卡片replay 中的 ThreadRolledBack + ContextManager rewrite下一次模型请求只看 surviving history。
thread list 需要分页每个 view 全量 replay JSONLState DB + metadata projection查询速度提升,但不替代 durable ledger。
turn 间设置变化假设文本包含执行语义TurnContextItem reference baselineresume/fork 可以恢复 cwd、model、permissions 和 diffs。

代价是 ceremony 更多:Codex 必须 persist、flush、project、reconstruct 和 baseline。 收益是每类失败都有明确 owner:模型看错 context,就看 ContextManager 与 rollout reconstruction;列表过期,就看 state projection;客户端在 setup 前渲染 event,就看 first-event invariant;resumed turn 执行语义不对,就看 TurnContextItem

常见误读

“rollout 就是模型 prompt。” 不是。rollout 是 durable replay ledger。它可以 包含会进入 prompt history 的 response item,但也包含 session metadata、turn context、compaction record 和 event。

“state database 是 source of truth。” 对 replay 不是。它是 query projection 和 operational state surface。本地实现可以在需要时从 rollout items backfill metadata。

SessionConfigured 只是 UI metadata。” 不是。manager 会在把 live CodexThread 插入 running thread map 之前,检查它是不是第一条 event。

“fork 就是复制所有可见消息。” fork 是从选定 replay prefix 打开一个新的未来。 可见 UI cards 不是语义边界。

TurnContextItem 只是重复正文。” 它携带执行语义:cwd、model、approval 与 sandbox policy、network constraints、instructions、schema 和 truncation policy。 这些字段无法从纯 message text 恢复。

应用到实践

  1. 先命名 owner,再命名数据:model view、replay ledger、query projection、client stream,还是 live scheduler。
  2. 让 live handle 保持窄边界;客户端提交 operation、接收 event,而不是直接修改 session 内部。
  3. 在后续 runtime facts 之前强制 setup event,让每个客户端都有稳定 stream anchor。
  4. 把 resume、fork、compaction 和 rollback 当成 durable items 上的 replay 操作,而不是 transcript edit。
  5. 分开持久化 execution context 和 query projection,并在设计允许时让 projection 可从 replay facts 修复。

小结

第 5 章把 Codex 的协议词汇变成了 durable runtime model。thread 拥有 identity 和 replay;session 拥有 live scheduling;Codex 暴露 queue pair;ContextManager 拥有模型可见视图;rollout items 保存 replay ledger;state DB 加速 query; TurnContextItem 保留执行语义。

下一步进入一次 live turn。session 已经存在,history 已经有 owner,但 Codex 还要 决定 user submission 如何变成 turn,tools 与 approvals 如何插入 model output,以及 runtime 如何判断 agent 是否真的完成。

源码地图

概念源码锚点
Thread store boundarycodex-rs/thread-store/src/store.rs
Thread persistence metadata and resume paramscodex-rs/thread-store/src/types.rs
Live thread handle and init guardcodex-rs/thread-store/src/live_thread.rs
Thread manager live map and shared servicescodex-rs/core/src/thread_manager.rs
Client-facing thread handlecodex-rs/core/src/codex_thread.rs
Resume-aware thread spawncodex-rs/core/src/thread_manager.rs
SessionConfigured first-event checkcodex-rs/core/src/thread_manager.rs
Queue-pair Codex facadecodex-rs/core/src/session/mod.rs
Live Session fieldscodex-rs/core/src/session/session.rs
Startup setup event orderingcodex-rs/core/src/session/session.rs
Model-visible history managercodex-rs/core/src/context_manager/history.rs
Rollout item vocabularycodex-rs/protocol/src/protocol.rs
Local rollout writercodex-rs/rollout/src/recorder.rs
Local state DB and metadata backfillcodex-rs/rollout/src/state_db.rs, metadata.rs
Initial history modescodex-rs/protocol/src/protocol.rs
Initial history recording and reconstruction installcodex-rs/core/src/session/mod.rs
Rollout reconstruction and rollback replaycodex-rs/core/src/session/rollout_reconstruction.rs, rollout_reconstruction.rs
Fork rollout truncationcodex-rs/core/src/thread_rollout_truncation.rs
Turn context runtime fieldscodex-rs/core/src/session/turn_context.rs
Turn context persisted itemcodex-rs/core/src/session/turn_context.rs, protocol.rs
Turn context baseline persistencecodex-rs/core/src/session/mod.rs