fix(exit): diagnostics after the terminal may be gone are best-effort writes

With fd 2 dead (a closed pane, an exited pipe reader) every write fails,
eprintln! panics on that failure, and under panic = "abort" the process
dies by SIGABRT with a crash report on the next launch instead of its
exit code. Exit reports, headless plain-format diagnostics, update-exit,
setup-result and leader-management messages now write through
best_effort_stderr::eprint_line: one attempt, failure reported, never a
panic.

Ported from upstream 1.0.19 (grok-build best_effort_stderr.rs).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
This commit is contained in:
雷电芽衣 committed 2026-09-13 10:39:20 -04:00
1 parent 901203c2b2
commit 17d4ba08f9
4 files changed
+95 -35

No files matched your search

+12 -9
View File
@@ -4,6 +4,7 @@
//! `spawn_kigi_shell`, sends the ACP lifecycle (init → auth → session → prompt),
//! streams text to stdout, and exits cleanly via `CancellationToken`.
use crate::best_effort_stderr::eprint_line;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
@@ -459,7 +460,7 @@ impl HeadlessEmitter {
fn on_error(&self, message: &str) {
match self.format {
OutputFormat::Plain => eprintln!("{message}"),
OutputFormat::Plain => eprint_line(message),
OutputFormat::StreamingJson | OutputFormat::Json => {
let mut err = serde_json::json!({"type":"error","message": message});
if let Some(usage) = &self.usage {
@@ -1276,7 +1277,7 @@ pub async fn run_single_turn(
== Some("max_turns_reached");
if is_max_turns {
match emitter.format {
OutputFormat::Plain => eprintln!("Max turns reached"),
OutputFormat::Plain => eprint_line("Max turns reached"),
OutputFormat::StreamingJson => {
println!("{}", serde_json::json!({"type": "max_turns_reached"}))
}
@@ -1644,7 +1645,9 @@ fn handle_ext_notification(
);
}
OutputFormat::Plain => {
eprintln!("Auto-compacting conversation ({percentage}% full)...");
eprint_line(&format!(
"Auto-compacting conversation ({percentage}% full)..."
));
}
OutputFormat::Json => {}
},
@@ -1652,7 +1655,7 @@ fn handle_ext_notification(
OutputFormat::StreamingJson => {
println!("{}", serde_json::json!({"type": "auto_compact_completed"}));
}
OutputFormat::Plain => eprintln!("Conversation compacted."),
OutputFormat::Plain => eprint_line("Conversation compacted."),
OutputFormat::Json => {}
},
XaiUpdate::AutoCompactFailed { error } => match format {
@@ -1664,9 +1667,9 @@ fn handle_ext_notification(
}
OutputFormat::Plain => {
if error.trim().is_empty() {
eprintln!("Auto-compact failed.");
eprint_line("Auto-compact failed.");
} else {
eprintln!("Auto-compact failed: {error}");
eprint_line(&format!("Auto-compact failed: {error}"));
}
}
OutputFormat::Json => {}
@@ -1675,7 +1678,7 @@ fn handle_ext_notification(
OutputFormat::StreamingJson => {
println!("{}", serde_json::json!({"type": "auto_compact_cancelled"}));
}
OutputFormat::Plain => eprintln!("Auto-compact cancelled."),
OutputFormat::Plain => eprint_line("Auto-compact cancelled."),
OutputFormat::Json => {}
},
XaiUpdate::AutoContinueCompleted { total_tokens } => match format {
@@ -1685,7 +1688,7 @@ fn handle_ext_notification(
serde_json::json!({"type": "auto_continue_completed", "total_tokens": total_tokens})
);
}
OutputFormat::Plain => eprintln!("Resumed after compaction."),
OutputFormat::Plain => eprint_line("Resumed after compaction."),
OutputFormat::Json => {}
},
XaiUpdate::ImageCompressed { message } => match format {
@@ -1695,7 +1698,7 @@ fn handle_ext_notification(
serde_json::json!({"type": "image_compressed", "message": message})
);
}
OutputFormat::Plain => eprintln!("{message}"),
OutputFormat::Plain => eprint_line(&message),
OutputFormat::Json => {}
},
XaiUpdate::SubagentSpawned { subagent_id } => {