chore(upstream-merge): sync with upstream/main by bucket; adopt exec updates and reconcile to fork protocol\n\n- Prefer-theirs: exec crate (cli flag + event processor)\n- Reconcile protocol mismatches (StreamErrorEvent removed, WebSearchComplete, stdout/stderr, UpdatePlanArgs{name,plan})\n- Preserve fork invariants (browser/agent tools, UA/version, TUI ordering)\n\nBuild: ./build-fast.sh OK; verify.sh OK (no warnings)

This commit is contained in:
just-every-code
2025-09-24 01:21:01 +00:00
3 changed files with 27 additions and 172 deletions
+4
View File
@@ -76,6 +76,10 @@ pub struct Cli {
#[arg(long = "json", default_value_t = false)]
pub json: bool,
/// Whether to include the plan tool in the conversation.
#[arg(long = "include-plan-tool", default_value_t = false)]
pub include_plan_tool: bool,
/// Specifies file where the last message from the agent should be written.
#[arg(long = "output-last-message")]
pub last_message_file: Option<PathBuf>,
@@ -20,7 +20,6 @@ use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::PatchApplyEndEvent;
use codex_core::protocol::SessionConfiguredEvent;
// Stream errors are surfaced via Error events in our fork
use codex_core::protocol::TaskCompleteEvent;
use codex_protocol::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
@@ -66,7 +65,6 @@ pub(crate) struct EventProcessorWithHumanOutput {
reasoning_started: bool,
raw_reasoning_started: bool,
last_message_path: Option<PathBuf>,
answer_bullet_filter: LeadingBulletFilter,
}
impl EventProcessorWithHumanOutput {
@@ -95,7 +93,6 @@ impl EventProcessorWithHumanOutput {
reasoning_started: false,
raw_reasoning_started: false,
last_message_path,
answer_bullet_filter: LeadingBulletFilter::default(),
}
} else {
Self {
@@ -114,7 +111,6 @@ impl EventProcessorWithHumanOutput {
reasoning_started: false,
raw_reasoning_started: false,
last_message_path,
answer_bullet_filter: LeadingBulletFilter::default(),
}
}
}
@@ -129,99 +125,6 @@ struct PatchApplyBegin {
auto_approved: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LeadingBulletState {
Pending,
Removed,
Keep,
}
impl Default for LeadingBulletState {
fn default() -> Self {
Self::Pending
}
}
#[derive(Default)]
struct LeadingBulletFilter {
state: LeadingBulletState,
buffer: String,
}
impl LeadingBulletFilter {
fn reset(&mut self) {
self.state = LeadingBulletState::Pending;
self.buffer.clear();
}
fn ingest_delta(&mut self, delta: &str) -> Option<String> {
match self.state {
LeadingBulletState::Pending => {
self.buffer.push_str(delta);
self.finalize_pending()
}
LeadingBulletState::Removed | LeadingBulletState::Keep => Some(delta.to_string()),
}
}
fn sanitize_full(&mut self, text: &str) -> String {
self.reset();
self.buffer.push_str(text);
match self.finalize_pending() {
Some(out) => out,
None => {
if self.buffer == "-" {
self.buffer.clear();
self.state = LeadingBulletState::Removed;
String::new()
} else {
self.state = LeadingBulletState::Keep;
std::mem::take(&mut self.buffer)
}
}
}
}
fn finalize_pending(&mut self) -> Option<String> {
if self.buffer.is_empty() {
return Some(String::new());
}
let mut chars = self.buffer.char_indices();
let Some((_, first)) = chars.next() else {
return Some(String::new());
};
if first != '-' {
self.state = LeadingBulletState::Keep;
return Some(std::mem::take(&mut self.buffer));
}
match chars.next() {
Some((second_idx, second_char)) => {
if matches!(second_char, ' ' | '\t') {
let drain_end = second_idx + second_char.len_utf8();
self.buffer.drain(..drain_end);
self.state = LeadingBulletState::Removed;
Some(std::mem::take(&mut self.buffer))
} else if matches!(second_char, '\n' | '\r') {
self.buffer.drain(..second_idx);
self.state = LeadingBulletState::Removed;
Some(std::mem::take(&mut self.buffer))
} else if second_char.is_whitespace() {
let drain_end = second_idx + second_char.len_utf8();
self.buffer.drain(..drain_end);
self.state = LeadingBulletState::Removed;
Some(std::mem::take(&mut self.buffer))
} else {
self.state = LeadingBulletState::Keep;
Some(std::mem::take(&mut self.buffer))
}
}
None => None,
}
}
}
// Timestamped println helper. The timestamp is styled with self.dimmed.
#[macro_export]
macro_rules! ts_println {
@@ -284,19 +187,11 @@ impl EventProcessor for EventProcessorWithHumanOutput {
return CodexStatus::InitiateShutdown;
}
EventMsg::TokenCount(ev) => {
if let Some(info) = &ev.info {
if let Some(usage_info) = ev.info {
ts_println!(
self,
"tokens used: {}",
format_with_separators(info.total_token_usage.blended_total())
);
}
if let Some(snapshot) = &ev.rate_limits {
ts_println!(
self,
"rate limits: hourly {:.1}% • secondary {:.1}%",
snapshot.primary_used_percent,
snapshot.secondary_used_percent
format_with_separators(usage_info.total_token_usage.blended_total())
);
}
}
@@ -304,15 +199,10 @@ impl EventProcessor for EventProcessorWithHumanOutput {
if !self.answer_started {
ts_println!(self, "{}\n", "codex".style(self.italic).style(self.magenta));
self.answer_started = true;
self.answer_bullet_filter.reset();
}
if let Some(out) = self.answer_bullet_filter.ingest_delta(&delta) {
if !out.is_empty() {
print!("{out}");
#[expect(clippy::expect_used)]
std::io::stdout().flush().expect("could not flush stdout");
}
}
print!("{delta}");
#[expect(clippy::expect_used)]
std::io::stdout().flush().expect("could not flush stdout");
}
EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) => {
if !self.show_agent_reasoning {
@@ -368,18 +258,15 @@ impl EventProcessor for EventProcessorWithHumanOutput {
// if answer_started is false, this means we haven't received any
// delta. Thus, we need to print the message as a new answer.
if !self.answer_started {
let sanitized = self.answer_bullet_filter.sanitize_full(&message);
ts_println!(
self,
"{}\n{}",
"codex".style(self.italic).style(self.magenta),
sanitized,
message,
);
self.answer_bullet_filter.reset();
} else {
println!();
self.answer_started = false;
self.answer_bullet_filter.reset();
}
}
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
@@ -414,6 +301,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
} else {
("".to_string(), format!("exec('{call_id}')"))
};
let truncated_stdout = stdout
.lines()
.take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL)
@@ -609,13 +497,8 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => {
let suppress = std::env::var("CODE_SUPPRESS_TURN_DIFF")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !suppress {
ts_println!(self, "{}", "turn diff:".style(self.magenta));
println!("{unified_diff}");
}
ts_println!(self, "{}", "turn diff:".style(self.magenta));
println!("{unified_diff}");
}
EventMsg::ExecApprovalRequest(_) => {
// Should we exit?
@@ -668,17 +551,6 @@ impl EventProcessor for EventProcessorWithHumanOutput {
EventMsg::AgentStatusUpdate(_) => {
// Currently ignored in exec output.
}
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
TurnAbortReason::Interrupted => {
ts_println!(self, "task interrupted");
}
TurnAbortReason::Replaced => {
ts_println!(self, "task aborted: replaced by a new task");
}
TurnAbortReason::ReviewEnded => {
ts_println!(self, "task aborted: review ended");
}
},
EventMsg::CustomToolCallBegin(event) => {
ts_println!(
self,
@@ -724,6 +596,17 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
}
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
TurnAbortReason::Interrupted => {
ts_println!(self, "task interrupted");
}
TurnAbortReason::Replaced => {
ts_println!(self, "task aborted: replaced by a new task");
}
TurnAbortReason::ReviewEnded => {
ts_println!(self, "task aborted: review ended");
}
},
EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
EventMsg::ConversationPath(_) => {}
EventMsg::UserMessage(_) => {}
@@ -741,7 +624,7 @@ fn escape_command(command: &[String]) -> String {
fn format_file_change(change: &FileChange) -> &'static str {
match change {
FileChange::Add { .. } => "A",
FileChange::Delete => "D",
FileChange::Delete { .. } => "D",
FileChange::Update {
move_path: Some(_), ..
} => "R",
@@ -768,36 +651,3 @@ fn format_mcp_invocation(invocation: &McpInvocation) -> String {
format!("{fq_tool_name}({args_str})")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_sanitizes_full_message() {
let mut filter = LeadingBulletFilter::default();
let sanitized = filter.sanitize_full("- Quick summary\nMore");
assert_eq!(sanitized, "Quick summary\nMore");
}
#[test]
fn filter_handles_multi_delta_stream() {
let mut filter = LeadingBulletFilter::default();
assert!(filter.ingest_delta("-").is_none(), "first dash should defer");
let out = filter
.ingest_delta(" Lead")
.expect("delta should produce output");
assert_eq!(out, "Lead");
let subsequent = filter
.ingest_delta("ing continues")
.expect("subsequent delta");
assert_eq!(subsequent, "ing continues");
}
#[test]
fn filter_keeps_non_bullet_prefix() {
let mut filter = LeadingBulletFilter::default();
let sanitized = filter.sanitize_full("-value remains");
assert_eq!(sanitized, "-value remains");
}
}
+2 -1
View File
@@ -53,6 +53,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
sandbox_mode: sandbox_mode_cli_arg,
prompt,
output_schema: output_schema_path,
include_plan_tool,
config_overrides,
..
} = cli;
@@ -162,7 +163,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
model_provider,
codex_linux_sandbox_exe,
base_instructions: None,
include_plan_tool: None,
include_plan_tool: Some(include_plan_tool),
include_apply_patch_tool: None,
include_view_image_tool: None,
disable_response_storage: None,