Claude Code Direct 回合补 sidecar 静默与超时收口诊断
Project CI / AI game creator shell Rust crates (push) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m39s
Project CI / Backend tests (push) Failing after 3m28s
Project CI / Frontend tests (push) Successful in 1m51s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m39s
Project CI / Backend tests (push) Failing after 3m28s
Project CI / Frontend tests (push) Successful in 1m51s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- claude_code_cli:sidecar stdout 静默 30 秒就写一条 agent.direct_codex.claude_sidecar_idle,记录已静默秒数 - claude_code_cli:超时收口先杀 sidecar 进程树,再给 stderr 一个 5 秒排空窗口,避免回合永远不肯收口 - claude_code_cli:超时详情过滤掉宿主主动断流后 sidecar 自己抛的 EPIPE 收尾噪声 - claude_code_cli:sidecar 返回 error 事件时同样杀进程树,不再留下孤儿 Claude Code 进程
This commit is contained in:
@@ -17,6 +17,18 @@ const CLAUDE_NODE_ENV: &str = "GENARRATIVE_CLAUDE_NODE";
|
||||
const CLAUDE_CODE_OUTPUT_MAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
const CLAUDE_CODE_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
const CLAUDE_CODE_MODEL_MAX_CHARS: usize = 200;
|
||||
/// 超时收口时给 sidecar stderr 的排空窗口。
|
||||
///
|
||||
/// sidecar 自己还活着时 stderr 永远等不到 EOF(Claude Code CLI 正在网关侧重试),
|
||||
/// 没有上限就会让超时分支自己挂住:这一轮既写不出终态,也留不下任何诊断
|
||||
/// (2026-10-02 实测:回合 4 分钟不结束、应用日志一行不写)。
|
||||
const CLAUDE_CODE_STDERR_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
/// sidecar stdout 静默多久就写一条"还没收到事件"的应用日志。
|
||||
///
|
||||
/// Claude Code CLI 在网关侧失败时会自己退避重试,这段时间**一行事件都没有**;
|
||||
/// 不留痕的话用户与离线诊断只能看到一个卡住的回合(真实案例:ECONNRESET 重试,
|
||||
/// 每轮 ~60 秒,共 10 轮)。
|
||||
const CLAUDE_CODE_EVENT_IDLE_LOG_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
struct ActiveClaudeCodeTurn {
|
||||
client_turn_id: String,
|
||||
@@ -216,11 +228,26 @@ async fn run_sidecar_turn(
|
||||
let mut lines = BufReader::new(stdout).lines();
|
||||
let mut events = Vec::new();
|
||||
let mut terminal = None;
|
||||
while let Some(line) = lines
|
||||
.next_line()
|
||||
.await
|
||||
.map_err(|error| format!("读取 Claude Agent SDK sidecar 输出失败:{error}"))?
|
||||
{
|
||||
let mut idle_rounds = 0_u64;
|
||||
loop {
|
||||
let line =
|
||||
match tokio::time::timeout(CLAUDE_CODE_EVENT_IDLE_LOG_TIMEOUT, lines.next_line())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(Some(line))) => line,
|
||||
Ok(Ok(None)) => break,
|
||||
Ok(Err(error)) => {
|
||||
return Err(format!("读取 Claude Agent SDK sidecar 输出失败:{error}"));
|
||||
}
|
||||
Err(_) => {
|
||||
idle_rounds += 1;
|
||||
app_log!(
|
||||
"agent.direct_codex.claude_sidecar_idle idleSeconds={}",
|
||||
CLAUDE_CODE_EVENT_IDLE_LOG_TIMEOUT.as_secs() * idle_rounds
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -237,6 +264,7 @@ async fn run_sidecar_turn(
|
||||
break;
|
||||
}
|
||||
Some("error") => {
|
||||
kill_claude_code_process_tree(pid);
|
||||
return Err(value
|
||||
.get("message")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
@@ -260,13 +288,19 @@ async fn run_sidecar_turn(
|
||||
{
|
||||
Ok(result) => result?,
|
||||
Err(_) => {
|
||||
let detail = stderr_task
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
// 先杀进程树:不杀的话 sidecar(连同它守护的 Claude Code CLI)会一直活着,
|
||||
// stderr 等不到 EOF,这一轮就永远出不了终态。
|
||||
kill_claude_code_process_tree(pid);
|
||||
let stderr_detail =
|
||||
match tokio::time::timeout(CLAUDE_CODE_STDERR_DRAIN_TIMEOUT, stderr_task).await {
|
||||
Ok(Ok(Ok(bytes))) => String::from_utf8(bytes).ok(),
|
||||
_ => None,
|
||||
}
|
||||
.map(|value| value.trim().chars().take(512).collect::<String>())
|
||||
.filter(|value| !value.is_empty())
|
||||
// 断开 stdout 之后 sidecar 自己会抛 EPIPE;那是收尾噪声,不是失败原因。
|
||||
.filter(|value| !value.contains("EPIPE"));
|
||||
let detail = stderr_detail
|
||||
.map(|value| format!(":{value}"))
|
||||
.unwrap_or_default();
|
||||
return Err(format!("Claude Agent SDK sidecar 回合超时{detail}"));
|
||||
|
||||
Reference in New Issue
Block a user