Skip to content

Thread de session et historique des messages ​

源码版本rust-v0.145.0

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 ​

  1. Recevoir les Op externes (messages utilisateur, steer, cancel, shutdown), et les pousser via submit / submit_with_trace dans la mailbox de Session (codex-rs/core/src/codex_thread.rs:205-275).
  2. Exposer le flux d'événements : next_event récupère un Event ; agent_status replie les événements en AgentStatus pour que l'UI affiche idle/running/completed (codex-rs/core/src/codex_thread.rs:414-422).
  3. Injecter des items visibles du modèle : inject_response_items pousse des ResponseItem dans l'historique sans lancer de nouveau turn ; inject_if_running intervient en plein turn (codex-rs/core/src/codex_thread.rs:306-483).
  4. Gérer le snapshot de config du thread : config_snapshot / preview_thread_settings_overrides packagent les runtime config sandbox, approval, environment, model pour l'amont (codex-rs/core/src/codex_thread.rs:355-361).
  5. 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_metadata traversent toutes LiveThread (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 :

rust
// 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 :

rust
// 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 :

rust
// 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_idle retourne PlanMode directement 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 : LiveThreadInitGuard appelle discard via Drop en 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_items refuse les items vides : retourne directement InvalidRequest, 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_entry renvoie return 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épasse max_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.