SessionRuntime与状态保持
一个 Code Mode session 的长期状态不在某个 V8 isolate 字段里,而在 SessionRuntime:stored_values 跨 cell 共享,cells 保存可观察 cell handle,TaskTracker 等待 actor 与 failure watcher 完成,shutdown token 控制所有 child cell。每个 cell 只拿启动快照,并维护自己的 write-set;completion commit 必须和 CellState 终态一起决定,避免终止竞态把候选值暴露给下一 cell。
本文承接CellActor与执行队列和V8Runtime初始化,面向理解 Arc、Mutex、TaskTracker、CancellationToken 和跨任务提交的读者。范围是 SessionRuntime 的状态持有、cell admission、stored values 和 shutdown,不展开 V8 API。当前动态测试受 rusty_v8 archive 404 阻塞,正文明确区分源码事实和测试结果。
1. Runtime所有者
源码位置:codex-rs/code-mode-runtime/src/session_runtime/mod.rs :: SessionRuntime、Inner
pub(crate) struct SessionRuntime<D: SessionRuntimeDelegate> {
inner: Arc<Inner<D>>,
}
struct Inner<D: SessionRuntimeDelegate> {
stored_values: Mutex<HashMap<String, JsonValue>>,
cells: Mutex<HashMap<CellId, CellHandle>>,
cell_tasks: TaskTracker,
shutdown_token: CancellationToken,
delegate: Arc<D>,
task_failure_handler: Option<TaskFailureHandler>,
next_cell_id: AtomicU64,
}2. Cell admission
execute 先检查 shutdown,再通过 atomic fetch_update 分配 ID,最后进入 start_cell。ID 在 prepare 失败时也不会回收,因此不会被另一 cell 重用。start_cell 先复制 stored snapshot,再取得 registry lock, 第二次检查 shutdown/duplicate,随后在锁内 prepare actor、插入 handle 并注册 TaskTracker。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/mod.rs :: execute、start_cell
if self.inner.shutdown_token.is_cancelled() {
return Err(Error::ShuttingDown);
}
let cell_id = self.allocate_cell_id()?;
let initial_event = self
.start_cell(cell_id.clone(), request, initial_observe_mode)
.await?;
Ok(StartedCell { cell_id, initial_event })外层检查只负责尽早拒绝已关闭的 runtime;真正决定是否能注册 cell 的逻辑在下一段锁内代码中。
let mut cells = self.inner.cells.lock().await;
if self.inner.shutdown_token.is_cancelled() {
return Err(Error::ShuttingDown);
}
let cell_state = Arc::new(CellState::new(
self.inner.shutdown_token.child_token(),
));
let (handle, initial_event, task) = CellActor::prepare(/* ... */)?;
cells.insert(cell_id.clone(), handle);
let task = self.inner.cell_tasks.spawn(task);若配置 task failure handler,SessionRuntime 还在同一个 TaskTracker 中启动 watcher,等待 actor JoinHandle; actor task panic 会附带 cell ID 上报。shutdown 因此等待 actor 和 watcher 两类任务,而不只是 V8 thread。
这两个片段分别对应 admission 的外层入口和 registry 锁内的最终提交点;只有同时阅读它们,才能看出第一次检查与第二次检查之间的竞态窗口。
第二次 shutdown 检查是关键:execute 可能在第一次检查后排队等待 cells mutex,shutdown 必须阻止它在关闭后完成 admission。
3. Cell ID与查找
ID 从 1 开始的 atomic counter 递增,溢出返回 CellIdSpaceExhausted,不会回绕制造重复 ID。observe/terminate 都从 cells map clone handle,再把 actor 错误映射为 Missing、Busy、AlreadyTerminating 或 Closed。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/mod.rs :: allocate_cell_id、begin_observe、terminate
fn allocate_cell_id(&self) -> Result<CellId, Error> {
self.inner
.next_cell_id
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| {
next.checked_add(1)
})
.map(|cell_id| CellId::new(cell_id.to_string()))
.map_err(|_| Error::CellIdSpaceExhausted)
}begin_observe 和 terminate 只在持锁时 clone CellHandle,随后释放 registry lock 再等待 actor future。 因此一次长 wait 不会阻塞其他 cell lookup;cell actor 最终调用 RuntimeCellHost::closed 才从 map 删除, 之后同一 ID 返回 MissingCell。
4. Stored values
4.1 读取快照
start_cell 创建 actor 时 clone stored_values,因此 cell 获得启动时快照;cell 执行中的写入不会直接修改全局 map,必须在 completion commit 阶段提交。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/mod.rs :: start_cell
let stored_values = self.inner.stored_values.lock().await.clone();
let host = Arc::new(RuntimeCellHost {
cell_id: cell_id.clone(),
inner: Arc::clone(&self.inner),
});cell 内的 store(key, value) 会同时更新 runtime-local stored_values 和 stored_value_writes。前者使同一 cell 后续 load(key) 立即看到新值,后者是最终提交给 SessionRuntime 的 write-set。值必须能转为普通 JSON;无法序列化的对象会抛 TypeError,不进入任何 map。
源码位置:codex-rs/code-mode-runtime/src/runtime/callbacks.rs :: store_callback、load_callback
state.stored_values.insert(key.clone(), serialized.clone());
state.stored_value_writes.insert(key, serialized);4.2 Completion提交
RuntimeCellHost 先等待 stored_values mutex,再在 CellState commit_completion 中确认 cancellation/phase;只有两者都允许时,才把 stored_value_writes 合并进 session map。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/mod.rs :: RuntimeCellHost::commit_completion
let cancellation_token = cell_state.cancellation_token();
let mut stored_values = tokio::select! {
biased;
_ = cancellation_token.cancelled() => {
return CompletionCommit::Rejected(event);
}
stored_values = self.inner.stored_values.lock() => stored_values,
};
cell_state.commit_completion(event, pending_initial_yield_items, || {
stored_values.extend(stored_value_writes);
})tokio::select! 使用 biased cancellation 分支;即使 map lock 同时 ready,已取消 cell 也优先 Reject。取得 map lock 后仍需 CellState::commit_completion 再确认 phase,形成 cancellation token 与终态 mutex 的双重 检查。只有 commit closure 内才执行 extend。
5. 状态隔离
同一 SessionRuntime 的 cell 启动时复制同一个 stored_values map,因此前一个 cell 成功提交的值可被后一个 cell load;不同 SessionRuntime 有不同 Inner,值不会共享。completion 被 termination 拒绝时,候选值不会进入 map。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/tests.rs :: termination_rejects_a_waiting_store_commit_before_the_next_cell_can_load_it
assert!(
!runtime
.inner
.stored_values
.lock()
.await
.contains_key("candidate")
);5.1 并发合并
两个并发 cell 可能从同一旧快照开始。completion 不会把整个 snapshot 写回,只把各自 stored_value_writes extend 到共享 map,因此 A 写 a、B 写 b 时两者都保留;如果都写同一个 key, 后完成者覆盖先完成者。当前没有版本号、compare-and-swap 或 conflict error。
源码位置:
codex-rs/code-mode-runtime/src/session_runtime/mod.rs::RuntimeCellHost::commit_completioncodex-rs/core/tests/suite/code_mode.rs::code_mode_concurrent_cells_merge_only_the_stored_values_they_write
6. Shutdown
显式 shutdown 先取消 root token并关闭 TaskTracker,再取得 cells registry lock,再次 close tracker,释放锁后 等待所有 cell task。获取 registry lock 的动作保证所有已通过 shutdown 第二次检查的 admission 都已注册 task。TaskTracker::close 可重复调用,因此 begin_shutdown 与 barrier 内 close 不冲突。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/mod.rs :: begin_shutdown、shutdown、Drop
pub(crate) async fn shutdown(&self) -> Result<(), Error> {
self.begin_shutdown();
let cells = self.inner.cells.lock().await;
self.inner.cell_tasks.close();
drop(cells);
self.inner.cell_tasks.wait().await;
Ok(())
}Drop for SessionRuntime 只能同步调用 begin_shutdown,不能 await actor task;它保证取消与禁止新 task, 真正清理由已启动 actor 自行完成。需要确定资源已经释放的调用方必须显式 await shutdown(),不能只 drop 最后一个 facade。
7. 验证边界
SessionRuntime tests 覆盖 cell actor panic 报告、store commit 被 termination 拒绝、ID 溢出、shutdown admission lock 和 drop 清理;service test 覆盖同 session 共享与跨 session 隔离;Core integration 覆盖并发 cell 只合并各自 write-set。当前动态测试需要 rusty_v8 archive,Apple Silicon 下载 404,未进入断言。
源码位置:codex-rs/code-mode-runtime/src/session_runtime/tests.rs
cd codex-rs
cargo test -p codex-code-mode-runtime --lib session_runtime::tests -- --test-threads=1
cargo test -p codex-code-mode-runtime --lib stored_values_are_shared_between_cells_but_not_sessions -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --test all code_mode_concurrent_cells_merge_only_the_stored_values_they_write -- --test-threads=1这些测试未执行时,不能把 write-set 合并、termination 优先或 shutdown registry barrier 写成动态通过; 正文描述的是当前源码不变量与测试预期,待 V8 构建恢复后仍需重跑。即使这些测试通过,也不证明 多进程 host 之间共享 store,或同一 key 的并发写入具有冲突检测;当前 store 只属于一个逻辑 session。
8. 源码排查
rg -n "struct Inner|stored_values|cells|cell_tasks|shutdown_token" codex-rs/code-mode-runtime/src/session_runtime/mod.rs
rg -n "allocate_cell_id|start_cell|commit_completion|closed\(" codex-rs/code-mode-runtime/src/session_runtime/mod.rs
rg -n "termination_rejects|cell_id_allocation|shutdown_rejects|drop_terminates" codex-rs/code-mode-runtime/src/session_runtime/tests.rsSessionRuntime 主线是:admission 分配不复用的 cell ID 并在 registry 锁下注册 actor,cell 启动时复制 stored snapshot,store 形成 local view 与 write-set,完成时在 cancellation/CellState 双重确认后 merge, 显式 shutdown 用 root cancellation、registry barrier 和 TaskTracker 等待所有 cell。下一篇 CodeMode工具调用桥接将分析 nested Promise 与 Core ToolCallRuntime 往返。
