セッションスレッドとメッセージ履歴
CodexThread は codex が上位(UI、app-server、extension)に晒すセッションハンドルだ。一つの thread は連続する対話履歴、一つの session loop、一つの rollout ファイル、そして紐付く作業ディレクトリと権限設定に対応する。それ自身はモデルループを走らせず、Op を裏側の Session に投げ、イベントストリームを呼び出し側に返すだけだ。実際に履歴を保存し、圧縮し、agent loop を走らせるのは Session とその LiveThread だ。
責務
- 外部
Op(ユーザメッセージ、steer、cancel、shutdown)を受け取り、submit/submit_with_traceでSessionの mailbox に投函する (codex-rs/core/src/codex_thread.rs:205-275)。 - イベントストリームの露出:
next_eventがEventを取り、agent_statusはイベントをAgentStatusに折り畳んで UI が idle/running/completed を表示できるようにする (codex-rs/core/src/codex_thread.rs:414-422)。 - モデル可視 item の注入:
inject_response_itemsはResponseItemを履歴に差し込むが新たに turn を始めず、inject_if_runningは turn 進行中に割り込む (codex-rs/core/src/codex_thread.rs:306-483)。 - thread 設定 snapshot の管理:
config_snapshot/preview_thread_settings_overridesが sandbox、approval、environment、model などのランタイム設定を上位にまとめて渡す (codex-rs/core/src/codex_thread.rs:355-361)。 - 永続化の代理:全ての rollout/thread-store 操作は
live_thread経由で転送され、load_history/read_thread/append_rollout_items/update_thread_metadataは全てLiveThreadを通る (codex-rs/core/src/codex_thread.rs:504-558)。
設計動機
初期 codex は「対話」と「セッションループ」を一つに結合していたが、後に CodexThread を facade として分離した。主な理由は extension と app-server が作業するための安定したハンドルを必要としたことだ——それらが Session 内部の channel を直接触るべきではない。CodexThread は SessionIo(下位 mpsc)を自分の中に封じ込み、submit と next_event の二方向だけを露出する。これがドキュメントの言う「bidirectional stream of messages that compose a thread」だ。
履歴永続化が thread-store crate として独立しているのは、実装を差し替え可能にするためだ:ローカル rollout ファイル、リモート service、インメモリ mock。ThreadStore trait はライフサイクルメソッド(create_thread / resume_thread / append_items / persist_thread / flush_thread / shutdown_thread / discard_thread)だけを規定し、コアコードはバックエンドがどこにあるかを気にしない。LiveThread は session が持つハンドルで、コアの「書くべきなら書く」判断を store 呼び出しに翻訳する。
message-history crate は別の次元だ:thread ごとの rollout ではなく、~/.codex/history.jsonl というグローバルに thread をまたぐ JSONL ファイルで、一行が {session_id, ts, text}。用途は TUI の履歴コマンド検索であって、対話の復元ではない。だから thread-store とは独立し、advisory lock と trim を自分で処理する。
TryStartTurnIfIdleError の設計は言及する価値がある:extension が「スレッドが空いたら自動で一 turn 始める」をやろうとする時、スレッドが忙しい、Plan モード、すでにユーザが発火した turn が並んでいる、といった状態が起こる。エラー戻り値には reason と元の items を不変で含め、呼び出し側が破棄するか再試行するか理由を記録するかを決める——入力を黙って飲み込むことはない。
主要ファイル
codex-rs/core/src/codex_thread.rs:162-203 — CodexThread 構造体定義と new コンストラクタ。Arc<Session>、SessionIo、SessionConfiguredEvent を保持。codex-rs/core/src/codex_thread.rs:86-124 — TryStartTurnIfIdleRejectionReason と TryStartTurnIfIdleError。自動 idle turn の三つの拒否理由。codex-rs/core/src/codex_thread.rs:446-483 — inject_user_message_without_turn と inject_response_items。履歴に差し込むが新しい turn は始めない。codex-rs/core/src/agent/status.rs:6-21 — agent_status_from_event。EventMsg を AgentStatus に折り畳む。codex-rs/thread-store/src/store.rs:36-94 — ThreadStore trait。全 thread 永続化バックエンドの契約。codex-rs/thread-store/src/live_thread.rs:35-108 — LiveThread 構造体と create。session が持つ store ハンドル。codex-rs/thread-store/src/live_thread.rs:48-90 — LiveThreadInitGuard。session 初期化失敗時に live writer を安全に破棄。codex-rs/message-history/src/lib.rs:104-189 — append_entry。グローバル history.jsonl へのアトミック追加。codex-rs/message-history/src/lib.rs:61-83 — HistoryEntry / HistoryConfig。グローバル履歴のデータ構造。CodexThread::submit は一見 self.io.submit(op).await 一行だが、io: SessionIo の背後には tokio mpsc + turn id 割り当て + trace context 注入がある。上位には「Op を投げて sub_id を受け取る」だけに見え、mailbox、turn queue、shutdown 信号は全て中に封じ込まれている:
// codex_thread.rs:205-207 — Op 的统一入口
pub async fn submit(&self, op: Op) -> CodexResult<String> {
self.io.submit(op).await
}agent 状態は CodexThread 自身が maintain するのではなく、EventMsg から派生する。TurnStarted -> Running、TurnComplete -> Completed、TurnAborted は原因で Interrupted か Errored に分かれ、ShutdownComplete -> Shutdown。is_final は PendingInit/Running/Interrupted を「まだ終わっていない」とし、他を終状態とする:
// agent/status.rs:6-21 — 状态机完全由事件驱动
pub(crate) fn agent_status_from_event(msg: &EventMsg) -> Option<AgentStatus> {
match msg {
EventMsg::TurnStarted(_) => Some(AgentStatus::Running),
EventMsg::TurnComplete(ev) => Some(AgentStatus::Completed(ev.last_agent_message.clone())),
EventMsg::TurnAborted(ev) => match ev.reason {
codex_protocol::protocol::TurnAbortReason::Interrupted
| codex_protocol::protocol::TurnAbortReason::BudgetLimited => {
Some(AgentStatus::Interrupted)
}
_ => Some(AgentStatus::Errored(format!("{:?}", ev.reason))),
},
EventMsg::Error(ev) => Some(AgentStatus::Errored(ev.message.clone())),
EventMsg::ShutdownComplete => Some(AgentStatus::Shutdown),
_ => None,
}
}グローバル history.jsonl の書き込みは興味深い詳細を持つ:ロックを保持したまま単一の write_all で JSON 全行 + \n を書き込まなければならず、そうして初めて POSIX は PIPE_BUF バイト以内の書き込みがアトミックであることを保証する。複数プロセスの並発追加が交錯しない。コードはわざわざ spawn_blocking でこの同期 IO を async runtime の外に移す:
// message-history/src/lib.rs:160-172 — 持锁、定位到末尾、一次写完
tokio::task::spawn_blocking(move || -> Result<()> {
for _ in 0..MAX_RETRIES {
match history_file.try_lock() {
Ok(()) => {
history_file.seek(SeekFrom::End(0))?;
history_file.write_all(line.as_bytes())?;
history_file.flush()?;
enforce_history_limit(&mut history_file, history_max_bytes)?;
return Ok(());
}
Err(std::fs::TryLockError::WouldBlock) => {
std::thread::sleep(RETRY_SLEEP);
}
Err(e) => return Err(e.into()),
}
}
// ...
})データフロー
境界と失敗
- Plan モードは自動 turn を拒否:
try_start_turn_if_idleは Plan モードでは即座にPlanModeを返す。Plan モードは自動的なモデル turn 開始を禁じ、ユーザの明示発火のみを許す (codex-rs/core/src/codex_thread.rs:313-331)。 - 初期化失敗時は writer を破棄:
LiveThreadInitGuardは session 初期化の途中で失敗した場合、Dropでdiscardを呼び、in-memory キューを durable にフラッシュすることを強要しない。tokio runtime が無ければ spawn 失敗時に warn に格下げする (codex-rs/thread-store/src/live_thread.rs:75-90)。 inject_response_itemsは空 items を拒否:直接InvalidRequestを返し、空の rollout を黙って一回書くのを避ける (codex-rs/core/src/codex_thread.rs:460-483)。- history.jsonl は敏感内容を永続化しない:
HistoryPersistence::Noneの場合、append_entryは即座にreturn Ok(())する。コードコメントには敏感モードをチェックする TODO が残っている (codex-rs/message-history/src/lib.rs:109-117)。 - trim は soft cap:
HISTORY_SOFT_CAP_RATIO = 0.8。ファイルがmax_bytesを超えても上限ちょうどに切るのではなく 80% まで切り詰め、再 trim の頻度を減らす (codex-rs/message-history/src/lib.rs:55-59)。
まとめ
CodexThread の核心価値は「一つのセッション」を安定したハンドルに抽象化することだ——バックエンドがローカル rollout でもリモート service でも、sub-agent があってもなくても、上位コードは submit / next_event / config_snapshot だけを見る。実際のループロジックは Agent メインループ に、永続化の詳細は thread-store trait の各実装にある。turn 内部の履歴がどう圧縮されるかを見るには コンテキスト圧縮の進化 に続く。