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

源码边界: 本章直接源码判断固定在 OpenAI Codex commit
569ff6a1c400bd514ff79f5f1050a684dc3afde3。
ThreadStore、LiveThread、ThreadManager、Codex、Session、
ContextManager、RolloutItem、InitialHistory、TurnContext 与
TurnContextItem 只有在正文链接到固定源码时才作为 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 需要的动词:
create、
resume、
append_items、
persist、
flush,
以及 shutdown / discard。初始化失败还有单独的
LiveThreadInitGuard:
如果 persistence 已经打开,但 session 构建失败,guard 会 discard live writer。
这里的实际规则很硬:打开 live writer 不等于 session 已经成功暴露。直到 session 真正接管 handle 之前,初始化都必须可回滚。
二、运行时栈是一组 handle
2.1 ThreadManager 拥有 live threads
下一层是
ThreadManager。
它持有 ThreadId 到
CodexThread
的 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.rs
把 Codex 描述为一个高层接口: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

SessionConfigured 是事件流锚点。后续启动工作可以继续,但客户端先拿到已解析的 thread/session envelope。3.1 SessionConfigured 打开事件流
Codex::spawn 返回后,manager 不会立刻发布新 thread。它先读取下一个 event,
并要求它是 SessionConfigured。
finalize_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_id、thread_id、forked_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

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,它的命令通道接收 AddItems、Persist、Flush 和 Shutdown
(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 的规则可以压成这张表:
| Surface | Owner | 主要问题 | 如果混成一个东西 |
|---|---|---|---|
| 模型可见视图 | ContextManager | 下一次模型请求应该看到什么? | resume 可能把 stale、compacted 或 rolled-back item 泄进 inference。 |
| Replay ledger | RolloutItem JSONL / store | durable order 里发生过什么? | fork 和 rollback 缺少重建事实。 |
| Query projection | State DB / metadata builders | 什么信息要高效 list/search/index? | thread list 要么全量 replay,要么 drift。 |
| Client stream | Event / EventMsg | 客户端现在应该渲染什么? | UI timing 变成事实来源,替代 durable replay。 |
五、Resume 与 fork 是 replay 选择

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 是执行语义信封

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 array | InitialHistory::Resumed + rollout reconstruction | 模型可见 history 从 replay facts 重建。 |
| 用户 fork 工作 | 复制 UI transcript | prefix selection + InitialHistory::Forked | 新 thread future 从一致 replay prefix 分叉。 |
| rollback 移除工作 | 删除最新可见卡片 | replay 中的 ThreadRolledBack + ContextManager rewrite | 下一次模型请求只看 surviving history。 |
| thread list 需要分页 | 每个 view 全量 replay JSONL | State DB + metadata projection | 查询速度提升,但不替代 durable ledger。 |
| turn 间设置变化 | 假设文本包含执行语义 | TurnContextItem reference baseline | resume/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 恢复。
应用到实践
- 先命名 owner,再命名数据:model view、replay ledger、query projection、client stream,还是 live scheduler。
- 让 live handle 保持窄边界;客户端提交 operation、接收 event,而不是直接修改 session 内部。
- 在后续 runtime facts 之前强制 setup event,让每个客户端都有稳定 stream anchor。
- 把 resume、fork、compaction 和 rollback 当成 durable items 上的 replay 操作,而不是 transcript edit。
- 分开持久化 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 是否真的完成。