From 22a9fcf4a6713dd3f3f91953d5aeffbe0895ad12 Mon Sep 17 00:00:00 2001 From: Abeautifulsnow <1491444340@qq.com> Date: Sun, 28 Jun 2026 19:34:48 +0800 Subject: [PATCH] fix(codex): send exec prompt through stdin --- crates/dbx-core/src/ai_cli_agent.rs | 34 ++++++++++++++++++++++++++++- crates/dbx-core/src/ai_codex_cli.rs | 29 ++++++++++++++++++++---- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/crates/dbx-core/src/ai_cli_agent.rs b/crates/dbx-core/src/ai_cli_agent.rs index c578fe1001..9489ac9884 100644 --- a/crates/dbx-core/src/ai_cli_agent.rs +++ b/crates/dbx-core/src/ai_cli_agent.rs @@ -4,7 +4,7 @@ use crate::token_usage::TokenUsage; use serde_json::Value; use std::ffi::OsStr; use std::process::Stdio; -use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; use tokio::sync::Notify; @@ -34,6 +34,7 @@ pub enum CliAgentJsonlDialect { pub struct CliAgentProcessSpec { pub command: CliAgentCommandSpec, pub env: Vec<(String, String)>, + pub stdin: Option, pub dialect: CliAgentJsonlDialect, pub classify_spawn_error: fn(&str) -> String, pub classify_run_error: fn(&str) -> String, @@ -361,11 +362,18 @@ pub async fn run_cli_jsonl_agent( let mut child = command .args(&spec.command.args) .envs(spec.env.iter().map(|(key, value)| (key.as_str(), value.as_str()))) + .stdin(if spec.stdin.is_some() { Stdio::piped() } else { Stdio::null() }) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|e| (spec.classify_spawn_error)(&e.to_string()))?; + if let Some(input) = spec.stdin { + let mut stdin = child.stdin.take().ok_or_else(|| "CLI agent stdin not available".to_string())?; + stdin.write_all(input.as_bytes()).await.map_err(|e| format!("Failed to write CLI agent stdin: {e}"))?; + stdin.shutdown().await.map_err(|e| format!("Failed to close CLI agent stdin: {e}"))?; + } + let stdout = child.stdout.take().ok_or_else(|| "Failed to capture CLI agent stdout".to_string())?; let stderr = child.stderr.take().ok_or_else(|| "Failed to capture CLI agent stderr".to_string())?; let mut lines = BufReader::new(stdout).lines(); @@ -470,6 +478,7 @@ mod tests { ], }, env: vec![("DBX_TEST_ENV".to_string(), "from-env".to_string())], + stdin: None, dialect: CliAgentJsonlDialect::CodexExec, classify_spawn_error, classify_run_error, @@ -480,6 +489,28 @@ mod tests { assert_eq!(result, "from-env"); } + #[tokio::test] + async fn jsonl_agent_writes_stdin_to_child() { + let spec = CliAgentProcessSpec { + command: CliAgentCommandSpec { + program: "sh".to_string(), + args: vec![ + "-c".to_string(), + "input=$(cat); printf '%s\n' \"{\\\"type\\\":\\\"item.completed\\\",\\\"item\\\":{\\\"type\\\":\\\"agent_message\\\",\\\"text\\\":\\\"$input\\\"}}\" \"{\\\"type\\\":\\\"turn.completed\\\"}\"".to_string(), + ], + }, + env: Vec::new(), + stdin: Some("prompt from stdin".to_string()), + dialect: CliAgentJsonlDialect::CodexExec, + classify_spawn_error, + classify_run_error, + }; + + let result = run_cli_jsonl_agent(spec, &Notify::new(), |_| {}).await.unwrap(); + + assert_eq!(result, "prompt from stdin"); + } + #[tokio::test] async fn jsonl_error_kills_and_waits_for_child() { let pid_file = std::env::temp_dir().join(format!( @@ -495,6 +526,7 @@ mod tests { let spec = CliAgentProcessSpec { command: CliAgentCommandSpec { program: "sh".to_string(), args: vec!["-c".to_string(), script] }, env: Vec::new(), + stdin: None, dialect: CliAgentJsonlDialect::CodexExec, classify_spawn_error, classify_run_error, diff --git a/crates/dbx-core/src/ai_codex_cli.rs b/crates/dbx-core/src/ai_codex_cli.rs index dfe08e492e..fdfbf3ad64 100644 --- a/crates/dbx-core/src/ai_codex_cli.rs +++ b/crates/dbx-core/src/ai_codex_cli.rs @@ -409,7 +409,7 @@ fn codex_mcp_config_overrides(options: &CodexRunOptions) -> Vec { overrides } -pub fn build_codex_exec_command(config: &AiConfig, prompt: &str, options: &CodexRunOptions) -> CodexCommandSpec { +pub fn build_codex_exec_command(config: &AiConfig, _prompt: &str, options: &CodexRunOptions) -> CodexCommandSpec { let mut args = vec![ "exec".to_string(), "--json".to_string(), @@ -429,7 +429,7 @@ pub fn build_codex_exec_command(config: &AiConfig, prompt: &str, options: &Codex args.push(model.to_string()); } - args.push(prompt.to_string()); + args.push("-".to_string()); CodexCommandSpec { program: codex_program(config), args } } @@ -519,11 +519,22 @@ fn classify_codex_spawn_error(message: &str) -> String { if message.contains("No such file") || message.contains("not found") { "[codexNotInstalled] Codex CLI was not found. Install Codex CLI or set the Codex CLI path in DBX AI settings." .to_string() + } else if is_command_line_too_long_error(message) { + "[codexCommandLineTooLong] Codex CLI command line is too long. Update DBX so Codex prompts are sent through stdin instead of process arguments." + .to_string() } else { format!("[codexRunFailed] Failed to start Codex CLI: {message}") } } +fn is_command_line_too_long_error(message: &str) -> bool { + let lower = message.to_ascii_lowercase(); + message.contains("os error 206") + || message.contains("文件名或扩展名太长") + || lower.contains("filename or extension is too long") + || lower.contains("the filename or extension is too long") +} + fn classify_codex_run_error(stderr: &str) -> String { let lower = stderr.to_ascii_lowercase(); if lower.contains("not authenticated") || lower.contains("login") || lower.contains("auth") { @@ -558,6 +569,7 @@ pub async fn run_codex_agent( CliAgentProcessSpec { command, env, + stdin: Some(prompt.to_string()), dialect: CliAgentJsonlDialect::CodexExec, classify_spawn_error: classify_codex_spawn_error, classify_run_error: classify_codex_run_error, @@ -573,8 +585,8 @@ mod tests { #[cfg(not(windows))] use super::shell_quote; use super::{ - build_codex_exec_command, codex_cli_env, codex_enabled_tools, is_path_like_program, parse_codex_jsonl_event, - parse_codex_models, validate_codex_program, CodexRunOptions, DEFAULT_CODEX_MODELS, + build_codex_exec_command, classify_codex_spawn_error, codex_cli_env, codex_enabled_tools, is_path_like_program, + parse_codex_jsonl_event, parse_codex_models, validate_codex_program, CodexRunOptions, DEFAULT_CODEX_MODELS, }; #[cfg(not(windows))] use super::{codex_process_env, common_executable_dirs, merged_path_with_dir}; @@ -621,6 +633,8 @@ mod tests { assert_eq!(spec.program, "codex"); assert!(spec.args.contains(&"--json".to_string())); + assert_eq!(spec.args.last().map(String::as_str), Some("-")); + assert!(!spec.args.contains(&"hello".to_string())); assert!(!spec.args.contains(&"--model".to_string())); assert!(!spec.args.contains(&"--ask-for-approval".to_string())); assert!(spec.args.contains(&"mcp_servers.dbx.command=\"dbx-mcp-server\"".to_string())); @@ -939,6 +953,13 @@ mod tests { assert!(!models.iter().any(|model| model.id == "priority" || model.id == "Priority")); } + #[test] + fn classifies_windows_command_line_too_long_spawn_error() { + let err = classify_codex_spawn_error("文件名或扩展名太长。 (os error 206)"); + + assert!(err.contains("[codexCommandLineTooLong]")); + } + #[test] fn parses_codex_jsonl_events() { assert!(parse_codex_jsonl_event(r#"{"type":"thread.started","thread_id":"t"}"#).is_none());