Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ use crate::protocol::EventMsg;
use crate::protocol::ExecApprovalRequestEvent;
use crate::protocol::ExecCommandBeginEvent;
use crate::protocol::ExecCommandEndEvent;
use crate::protocol::ExecCommandSummary;
use crate::protocol::FileChange;
use crate::protocol::InputItem;
use crate::protocol::Op;
Expand Down Expand Up @@ -422,6 +423,7 @@ impl Session {
call_id: &str,
output: &ExecToolCallOutput,
is_apply_patch: bool,
cwd: &std::path::Path,
) {
let ExecToolCallOutput {
stdout,
Expand All @@ -443,12 +445,46 @@ impl Session {
success: *exit_code == 0,
})
} else {
// Build an optional summary for non-zero exit codes to reduce log noise downstream
let summary = if *exit_code != 0 {
let stderr_tail: String = output
.stderr
.text
.chars()
.rev()
.take(1024)
.collect::<String>()
.chars()
.rev()
.collect();
let stdout_tail: String = output
.stdout
.text
.chars()
.rev()
.take(1024)
.collect::<String>()
.chars()
.rev()
.collect();
Some(ExecCommandSummary {
cwd: cwd.to_path_buf(),
stderr_tail,
stdout_tail,
stdout_truncated_after_lines: output.stdout.truncated_after_lines,
stderr_truncated_after_lines: output.stderr.truncated_after_lines,
})
} else {
None
};

EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: call_id.to_string(),
stdout,
stderr,
duration: *duration,
exit_code: *exit_code,
summary,
})
};

Expand Down Expand Up @@ -518,6 +554,7 @@ impl Session {
&call_id,
borrowed,
is_apply_patch,
&begin_ctx.cwd,
)
.await;

Expand Down
20 changes: 20 additions & 0 deletions codex-rs/core/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,26 @@ pub struct ExecCommandEndEvent {
pub exit_code: i32,
/// The duration of the command execution.
pub duration: Duration,
/// Optional concise summary to aid logs when a command fails.
/// Includes cwd and tails of streams; present primarily for non-zero exits.
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<ExecCommandSummary>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ExecCommandSummary {
/// The working directory used for the command.
pub cwd: std::path::PathBuf,
/// Last characters of stderr (up to implementation-defined limit).
pub stderr_tail: String,
/// Last characters of stdout (up to implementation-defined limit).
pub stdout_tail: String,
/// If output was truncated after a number of lines, this carries that value.
#[serde(skip_serializing_if = "Option::is_none")]
pub stdout_truncated_after_lines: Option<u32>,
/// If output was truncated after a number of lines, this carries that value.
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr_truncated_after_lines: Option<u32>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
Expand Down
52 changes: 52 additions & 0 deletions codex-rs/core/tests/exec_summary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![expect(clippy::unwrap_used)]

use std::time::Duration;

use codex_core::protocol::{ExecCommandEndEvent, ExecCommandSummary};

#[test]
fn exec_end_event_summary_serialization_roundtrip_some() {
let event = ExecCommandEndEvent {
call_id: "call-123".into(),
stdout: "out".into(),
stderr: "err".into(),
exit_code: 1,
duration: Duration::from_secs(1),
summary: Some(ExecCommandSummary {
cwd: std::path::PathBuf::from("/tmp"),
stderr_tail: "e".into(),
stdout_tail: "o".into(),
stdout_truncated_after_lines: Some(10),
stderr_truncated_after_lines: None,
}),
};

let json = serde_json::to_string(&event).unwrap();
let de: ExecCommandEndEvent = serde_json::from_str(&json).unwrap();

assert_eq!(de.call_id, event.call_id);
assert_eq!(de.exit_code, 1);
assert!(de.summary.is_some());
let s = de.summary.unwrap();
assert_eq!(s.cwd, std::path::PathBuf::from("/tmp"));
assert_eq!(s.stdout_truncated_after_lines, Some(10));
assert_eq!(s.stderr_truncated_after_lines, None);
}

#[test]
fn exec_end_event_summary_serialization_roundtrip_none() {
let event = ExecCommandEndEvent {
call_id: "call-456".into(),
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
duration: Duration::from_millis(0),
summary: None,
};

let json = serde_json::to_string(&event).unwrap();
let de: ExecCommandEndEvent = serde_json::from_str(&json).unwrap();

assert_eq!(de.call_id, event.call_id);
assert!(de.summary.is_none());
}
1 change: 1 addition & 0 deletions codex-rs/exec/src/event_processor_with_human_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
stderr,
duration,
exit_code,
..
}) => {
let exec_command = self.call_id_to_command.remove(&call_id);
let (duration, call) = if let Some(ExecCommandBegin { command, .. }) = exec_command
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ impl ChatWidget<'_> {
duration: _,
stdout,
stderr,
..
}) => {
// Compute summary before moving stdout into the history cell.
let cmd = self.running_commands.remove(&call_id);
Expand Down