Support Codex subagent transcripts

This commit is contained in:
iris committed 2026-09-13 00:41:59 -04:00
1 parent 7d9df5d572
commit 83b113ef0f
6 files changed
+557 -38

No files matched your search

+24 -11
View File
@@ -1,9 +1,11 @@
//! Codex CLI sessions over its persistent app-server protocol.
//!
//! One app-server owns one conversation. Unlike `codex exec --json`, this
//! surface can interrupt a turn without killing the connection and can steer
//! an active turn through `turn/steer`. Its stdio is a fifo plus logs so both
//! the process and an in-flight turn survive an ai-server restart.
//! One app-server owns one conversation tree. Unlike `codex exec --json`,
//! this surface can interrupt a turn without killing the connection and can
//! steer an active turn through `turn/steer`. Child threads are multiplexed
//! onto the same stdout and routed into subagent transcripts. Its stdio is a
//! fifo plus logs so both the process and in-flight turns survive an
//! ai-server restart.
mod translate;
@@ -22,6 +24,7 @@ use super::driver::{
AttachmentRef, Driver, Event, EventSink, SessionStatus, Unqueued, store_image,
};
use super::process;
use super::subagent::Subagents;
use super::transport::{Launch, Streams, Transport};
use crate::config::{ProviderConfig, SessionConfig};
use translate::Translator;
@@ -96,6 +99,7 @@ struct Inner {
to_child: mpsc::UnboundedSender<String>,
transport: Transport,
session_dir: PathBuf,
subagents: Arc<Subagents>,
reading: AtomicBool,
}
@@ -110,6 +114,7 @@ impl CodexDriver {
transport: Transport,
session_dir: &Path,
sink: EventSink,
subagents: Arc<Subagents>,
) -> Result<Self> {
let mut state = read_state(session_dir);
let recorded = process::recorded(session_dir);
@@ -170,6 +175,7 @@ impl CodexDriver {
to_child,
transport,
session_dir: session_dir.to_path_buf(),
subagents,
reading: AtomicBool::new(true),
});
@@ -577,7 +583,11 @@ fn spawn_follower(inner: Arc<Inner>, record: process::Record) {
async fn follow(inner: Arc<Inner>, mut record: process::Record, mut offset: u64) {
let stdout = inner.session_dir.join(STDOUT_LOG);
let stderr = inner.session_dir.join(STDERR_LOG);
let mut translator = Translator::default();
let mut translator = Translator::new(
Arc::clone(&inner.subagents),
read_thread(&inner.session_dir),
inner.state.lock().unwrap().active_turn.is_some(),
);
// `offset` is the durable boundary after the last complete record. `read_at` may move beyond
// it while app-server is still writing one record. Image-bearing tool results can be several
// megabytes long, and rereading their incomplete prefix every 50 ms made arrival over ssh
@@ -664,7 +674,8 @@ fn handle_line(inner: &Arc<Inner>, translator: &mut Translator, line: &Value) {
}
let method = line.get("method").and_then(Value::as_str);
let params = line.get("params").unwrap_or(&Value::Null);
match method {
let parent = translator.is_parent(line);
match method.filter(|_| parent) {
Some("thread/started") => {
if let Some(thread) = params.pointer("/thread/id").and_then(Value::as_str) {
write_thread(&inner.session_dir, thread);
@@ -697,9 +708,6 @@ fn handle_line(inner: &Arc<Inner>, translator: &mut Translator, line: &Value) {
if item.get("type").and_then(Value::as_str) == Some("userMessage") {
announce_user(inner, item);
}
for event in image_events(inner, item) {
let _ = inner.sink.send(event);
}
}
Some("turn/completed") => {
let mut state = inner.state.lock().unwrap();
@@ -711,10 +719,15 @@ fn handle_line(inner: &Arc<Inner>, translator: &mut Translator, line: &Value) {
}
_ => {}
}
for event in translator.translate(line) {
let images = if method == Some("item/completed") {
image_events(inner, &params["item"])
} else {
Vec::new()
};
for event in translator.translate_with_prefix(line, images) {
let _ = inner.sink.send(event);
}
if method == Some("turn/completed") {
if parent && method == Some("turn/completed") {
dispatch_waiting(inner);
}
}