fix(exec): bounded post-kill reaps — D-state children can't wedge a turn

Port of upstream grok-build 1.0.1 (landed upstream 2026-08-06).

A child wedged in an uninterruptible kernel syscall (D-state) never
observes SIGKILL, and a background descendant can hold the output pipes
open past the kill; both previously stalled tool futures without bound.

- kigi-tty-utils: KILL_REAP_TIMEOUT (5s) + reap_killed_bounded — wait
  for an already-killed child at most the bound, then abandon the corpse
  to tokio's orphan reaper.
- local_terminal: pipe readers share their buffer with the spawner and
  join under the reap bound on both exit arms (aborted readers are
  awaited so no tail bytes race the take); the whole tree is enrolled in
  a ProcessGroup and group-killed on timeout; the timeout arm returns
  the synthetic result instead of waiting on the corpse. Grandchild
  kill + prompt-return pinned by a ported unix regression test.
- kigi-tools: reap_killed_search_child wrapper; grep's four kill sites
  reap bounded; glob's stderr goes to null (an unread pipe blocks rg on
  warning backpressure) and its truncation kill reaps bounded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
This commit is contained in:
ZacharyZhang-NYandClaude Fable 5 committed 2026-08-27 10:45:17 -04:00
1 parent 1ae803519e
commit 26d26e15c9
7 files changed
+228 -32

No files matched your search

@@ -29,6 +29,7 @@ pub use path_suggestions::format_not_found_error;
pub use remap::{remap_json_keys, remap_schema_properties, reverse_map};
pub use spawn::{
ProcessGroup, ProcessScope, detach_command, global_process_scope, new_process_group,
reap_killed_search_child,
};
pub use truncate::{
DEFAULT_SOFT_WRAP_WIDTH, ceil_char_boundary, estimate_tokens, floor_char_boundary,
@@ -8,3 +8,19 @@
pub use kigi_tty_utils::{
ProcessGroup, ProcessScope, detach_command, global_process_scope, new_process_group,
};
/// Reap an already-killed search child, bounded by
/// [`kigi_tty_utils::KILL_REAP_TIMEOUT`]; on `None` warn and leave the corpse
/// to tokio's orphan reaper.
pub async fn reap_killed_search_child(
child: &mut tokio::process::Child,
) -> Option<std::process::ExitStatus> {
let status = kigi_tty_utils::reap_killed_bounded(child, kigi_tty_utils::KILL_REAP_TIMEOUT).await;
if status.is_none() {
tracing::warn!(
reap_timeout_secs = kigi_tty_utils::KILL_REAP_TIMEOUT.as_secs(),
"killed search child not reaped (bound expired — likely uninterruptible kernel I/O — or wait failed); abandoning to the orphan reaper"
);
}
status
}