Skip to content

fix(wrapper): bound child bootstrap handshake with timeout and parent cancellation - #911

Merged
kxn merged 1 commit into
kxn:masterfrom
ThatQuixote:fix/bootstrap-timeout
Sep 11, 2026
Merged

kxn merged 1 commit into
kxn:masterfrom
ThatQuixote:fix/bootstrap-timeout

Conversation

@ThatQuixote

Copy link
Copy Markdown
Contributor

Closes #910

问题

三个 backend 的 headless bootstrap(codex/claude/opencode)在 synthetic initialize 握手处都是裸 ReadBytes,无 deadline,launch 路径也不响应 ctx 取消。子进程永远不应答时(实测案例:子进程启动链路被文件锁卡死),launch 永久阻塞,会话进程及整棵子进程树滞留;headless pool 持续拉起新实例,进程数无上限堆积,最终顶满 systemd TasksMax(实测 38071),之后该 cgroup 内所有 clone() 返回 EAGAIN,daemon 自身 Go runtime fatal error: newosproc,故障持续 13 小时直至人工重启。

修复

新增 runChildBootstrap 统一封装(所有调用点在 internal/app/wrapper/):

  • 用 wrapperBootstrapTimeout(15s,常量与 wrapperChildWaitTimeout 同处)约束握手;正常握手 1s 内完成,15s 已很宽裕;
  • 握手期间响应父 ctx 取消;
  • 超时或取消时 cancel 子进程 ctx——进程被杀、stdout 管道关闭,bootstrap 协程随即解除阻塞并排空到带缓冲 channel,不泄漏;
  • cmd.Wait() 仍由调用方错误路径负责,行为不变。

三个调用点(codex app_child_session.go、claude app_child_session_claude.go、opencode app_child_session_opencode.go)切换到该封装。

测试

  • 新增回归测试:
    • TestRunChildBootstrapReturnsBootstrappedStdout 正常握手透传;
    • TestRunChildBootstrapTimesOutWedgedChild 永不应答的子进程在超时后返回错误且子进程被 cancel;
    • TestRunChildBootstrapHonorsParentCancel 父 ctx 取消即时生效并 cancel 子进程;
  • go build ./...、go vet、gofmt 通过;go test ./internal/app/wrapper/ 与 go test ./... 全量通过;
  • scripts/check/no-local-paths.sh 通过。

… cancellation (kxn#910)

A child that never answers the synthetic initialize request blocked
launchCodexChildSession forever: the bootstrap read loops have no
deadline and the launch path ignored ctx. Each wedged launch left the
session process and its whole child tree resident, and repeated
headless launches accumulated them without bound until the cgroup hit
its task limit, after which every clone() in the unit failed with
EAGAIN (observed in the field as a 13-hour crash loop, 527 coredumps
of external commands and the daemon's own Go runtime dying with
"fatal error: newosproc").

Wrap all three backend bootstrap call sites (codex, claude, opencode)
in runChildBootstrap, which bounds the handshake with
wrapperBootstrapTimeout (15s), honours parent cancellation, and cancels
the child context on either so the process is killed and its stdout
pipe closes, unblocking the bootstrap goroutine.

Co-Authored-By: Claude <noreply@anthropic.com>
@kxn
kxn merged commit ca36799 into kxn:master Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] headless bootstrap 无超时:子进程 initialize 永不应答时会话进程无限滞留,可放大为整机任务数耗尽

3 participants