Skip to content

fix(executor): agent output truncated — cmd.Wait closes pipes before scanners drain #8

Description

@liuxiaocs7

Agent executor silently truncates output and loses the session ID

AgentExecutor.Run (internal/executor/agent.go) called cmd.Wait() while goroutines were still reading cmd.StdoutPipe() / cmd.StderrPipe(). Per the StdoutPipe contract, Wait closes the parent pipe ends as soon as the process exits — it is incorrect to call it before all reads complete. Output the scanners had not read yet is dropped silently: no error, no log. Under load the entire output is lost, including the session_id line.

中文描述

AgentExecutor.Run 在扫描 goroutine 还没读完 StdoutPipe/StderrPipe 时就调用了 cmd.Wait()。按 StdoutPipe 的文档约定,进程一退出 Wait 就会关闭父端管道——在读完成之前调用是错误的。扫描器尚未读到的输出会被静默丢弃:无报错、无日志。高负载下(如 CI 的 -race)全部输出都会丢失,包括 session_id 行,导致 agent 日志不完整、无法按 session 恢复。

Evidence

  • CI (go test -race): TestAgentExecutorFakeCommand failed with session="" on 2/2 runs of PR feat(llm): support the OpenAI Responses API protocol #4, while the identical code passed locally and on main's single run — the classic signature of a scheduling race, not a functional failure.
  • Minimal standalone repro of the same pattern (child echos 3 lines and exits, reader goroutine delayed 3 ms): old order lost the whole output in 60/100 runs; drain-first lost 0/100.

Impact

  • Production: under load, an agent run (claude / cursor / codex) can lose output lines and the session ID — auto-dev logs are incomplete and resume-by-session becomes impossible. No error surfaces anywhere.
  • CI: manifests as a flaky TestAgentExecutorFakeCommand failure that randomly blocks PRs.

Expected behavior

  1. The scanners drain the full pipe contents before the pipes are closed — output and session ID are always captured regardless of scheduling.
  2. A descendant process that inherits the pipe and lingers must not hang the run — bounded by a grace period, with a visible truncation note.

Acceptance criteria

  • Session line captured even when the child exits immediately after emitting
  • Grandchild output written after the parent exits is still captured
  • A lingering descendant returns within the grace bound and logs a truncation note
  • Regression tests fail against the old code and pass with the fix (go test -race ./internal/...)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions