Thread de session et historique des messages
CodexThread est le handle de session que codex expose à l'amont (UI, app-server, extension). Un thread correspond à un historique de conversation continu, à une session loop, à un fichier rollout, ainsi qu'à un ensemble de répertoires de travail et de configurations de permissions liés. Il ne fait pas tourner lui-même la boucle modèle ; il ne fait qu'envoyer les Op au Session sous-jacent et remonter le flux d'événements à l'appelant. C'est Session et le LiveThread qu'elle détient qui stocke l'historique, fait la compaction et tourne la boucle agent.
Responsabilités
- Recevoir les
Opexternes (messages utilisateur, steer, cancel, shutdown), et les pousser viasubmit/submit_with_tracedans la mailbox deSession(codex-rs/core/src/codex_thread.rs:205-275). - Exposer le flux d'événements :
next_eventrécupère unEvent;agent_statusreplie les événements enAgentStatuspour que l'UI affiche idle/running/completed (codex-rs/core/src/codex_thread.rs:414-422). - Injecter des items visibles du modèle :
inject_response_itemspousse desResponseItemdans l'historique sans lancer de nouveau turn ;inject_if_runningintervient en plein turn (codex-rs/core/src/codex_thread.rs:306-483). - Gérer le snapshot de config du thread :
config_snapshot/preview_thread_settings_overridespackagent les runtime config sandbox, approval, environment, model pour l'amont (codex-rs/core/src/codex_thread.rs:355-361). - Déléguer la persistance : toutes les opérations rollout/thread-store passent par
live_thread;load_history/read_thread/append_rollout_items/update_thread_metadatatraversent toutesLiveThread(codex-rs/core/src/codex_thread.rs:504-558).
Motivations de conception
Au début, codex couplait « conversation » et « boucle de session » ; plus tard, CodexThread a été extrait comme facade, principalement parce que extension et app-server ont besoin d'un handle stable pour travailler — ils ne doivent pas toucher directement aux channels internes de Session. CodexThread encapsule SessionIo (mpsc sous-jacent) et n'expose que les deux directions submit et next_event ; c'est ce que la doc appelle « bidirectional stream of messages that compose a thread ».
La persistance de l'historique est extraite dans le crate thread-store parce que l'implémentation peut changer : fichier rollout local, service distant, mock en mémoire. Le trait ThreadStore ne fixe que les méthodes de cycle de vie (create_thread / resume_thread / append_items / persist_thread / flush_thread / shutdown_thread / discard_thread), le code cœur se fiche de l'emplacement du backend. LiveThread est le handle détenu par session, chargé de traduire la décision cœur « écrire quand il faut » en appels de store.
Le crate message-history est sur une autre dimension : ce n'est pas le rollout stocké par thread, mais le fichier JSONL global cross-thread ~/.codex/history.jsonl, avec un entry {session_id, ts, text} par ligne. Il sert à la TUI pour retrouver les commandes historiques, pas à restaurer une conversation. Il est donc indépendant de thread-store, gère son propre advisory lock et son trim.
Le design de TryStartTurnIfIdleError mérite mention : quand une extension veut « démarrer automatiquement un tour quand le thread est idle », celui-ci peut être occupé, en Plan mode, ou avoir déjà un turn utilisateur en file. L'erreur retournée embarque reason et les items d'origine intacts, pour que l'appelant décide : jeter, retry ou tracer la cause — pas avaler l'entrée silencieusement.
Fichiers clés
codex-rs/core/src/codex_thread.rs:162-203 — définition de la structure CodexThread et constructeur new, porte Arc<Session>, SessionIo, SessionConfiguredEvent.codex-rs/core/src/codex_thread.rs:86-124 — TryStartTurnIfIdleRejectionReason et TryStartTurnIfIdleError, les trois raisons de refus d'un turn idle automatique.codex-rs/core/src/codex_thread.rs:446-483 — inject_user_message_without_turn et inject_response_items, poussent des choses dans l'historique sans lancer de turn.codex-rs/core/src/agent/status.rs:6-21 — agent_status_from_event, replie un EventMsg en AgentStatus.codex-rs/thread-store/src/store.rs:36-94 — le trait ThreadStore, contrat de tous les backends de persistance thread.codex-rs/thread-store/src/live_thread.rs:35-108 — structure LiveThread et create, handle de store détenu par session.codex-rs/thread-store/src/live_thread.rs:48-90 — LiveThreadInitGuard, jette proprement le live writer si l'init de session échoue.codex-rs/message-history/src/lib.rs:104-189 — append_entry, ajout atomique au history.jsonl global.codex-rs/message-history/src/lib.rs:61-83 — HistoryEntry / HistoryConfig, structures de données de l'historique global.CodexThread::submit paraît se résumer à self.io.submit(op).await, mais derrière io: SessionIo il y a tokio mpsc + attribution de turn id + injection de trace context. L'amont ne voit que « j'ai poussé un Op et récupéré un sub_id » ; mailbox, file de turn, signaux de shutdown sont encapsulés :
// codex_thread.rs:205-207 — Op 的统一入口
pub async fn submit(&self, op: Op) -> CodexResult<String> {
self.io.submit(op).await
}L'état de l'agent n'est pas maintenu par CodexThread lui-même, il dérive d'un EventMsg. TurnStarted -> Running, TurnComplete -> Completed, TurnAborted devient Interrupted ou Errored selon la raison, ShutdownComplete -> Shutdown. is_final considère PendingInit / Running / Interrupted comme « pas encore fini » ; tous les autres états sont terminaux :
// 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,
}
}Un détail intéressant sur l'écriture du history.jsonl global : il faut tenir le lock et écrire toute la ligne JSON + \n en un seul write_all, car POSIX garantit l'atomicité des écritures jusqu'à PIPE_BUF octets. Plusieurs processus qui écrivent en parallèle ne s'entrelacent pas. Le code utilise délibérément spawn_blocking pour sortir cet IO synchrone du runtime async :
// 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()),
}
}
// ...
})Flux de données
Limites et échecs
- Plan mode refuse les turns automatiques :
try_start_turn_if_idleretournePlanModedirectement en Plan mode, car ce mode interdit tout démarrage automatique de turn modèle ; seul l'utilisateur peut le déclencher explicitement (codex-rs/core/src/codex_thread.rs:313-331). - Échec d'init doit jeter le writer :
LiveThreadInitGuardappellediscardviaDropen cas d'échec à mi-chemin de l'init de session, sans forcer le flush de la file in-memory vers durable ; si aucun runtime tokio n'est disponible, le spawn échoue et dégrade en warn (codex-rs/thread-store/src/live_thread.rs:75-90). inject_response_itemsrefuse les items vides : retourne directementInvalidRequest, pour éviter une écriture rollout vide silencieuse (codex-rs/core/src/codex_thread.rs:460-483).- history.jsonl ne persiste pas de contenu sensible : avec
HistoryPersistence::None,append_entryrenvoiereturn Ok(())directement ; un TODO dans le code rappelle de vérifier le mode sensible (codex-rs/message-history/src/lib.rs:109-117). - Trim par soft cap :
HISTORY_SOFT_CAP_RATIO = 0.8; quand le fichier dépassemax_bytes, on ne tailore pas exactement à la limite mais à 80 %, pour réduire la fréquence des retrims (codex-rs/message-history/src/lib.rs:55-59).
Récapitulatif
La valeur centrale de CodexThread est d'abstraire « une session » en un handle stable — que le backend soit un rollout local ou un service distant, qu'il y ait ou non des sub-agent, le code amont ne voit que submit / next_event / config_snapshot. La vraie logique de boucle est dans Boucle principale de l'Agent, et le détail de persistance est dans les implémentations du trait thread-store. Pour voir comment l'historique est compacté au sein d'un turn, enchaînez sur Évolution de la compaction du contexte.