Files
multica/server/pkg/agent/stderr_tail.go
songlei 6f04a6d26b feat(agent): surface agent CLI stderr tail in failure messages (#1674)
Hoist the existing stderrTail ring-buffer (previously codex-only) into
a shared pkg/agent helper so every Backend that supervises a child CLI
can include the last ~2 KB of that CLI's stderr in Result.Error. Wire
the claude backend through the same path.

Motivation: claude on Windows occasionally exits with a non-zero status
after ~5–8 minutes of a single long-running tool_use, and right now the
daemon only reports "claude exited with error: exit status 3" /
"exit status 0x80000003" — useless for root-causing V8 aborts, Bun
panics, native-module OOMs, or any other CLI-side crash. With the tail
attached, the failure message carries the real signal (panic line, V8
assertion, stderr-printed HTTP error) all the way into the task row's
error field that users see in the API.

Renames withCodexStderr to withAgentStderr(msg, label, tail) so the
helper is self-documenting across providers.
2026-04-26 10:55:21 +08:00

73 lines
2.2 KiB
Go

package agent
import (
"io"
"strings"
"sync"
)
// agentStderrTailBytes bounds the stderr tail captured for inclusion in
// error messages when an agent CLI exits before emitting a structured
// error (e.g. V8 abort on Windows, Bun panic, OOM). Large enough to
// contain typical CLI error lines, small enough to stay sensible inside
// a task-level Result.Error string.
const agentStderrTailBytes = 2048
// stderrTail forwards writes to an inner writer (typically the daemon's
// log) while also retaining a bounded tail of the bytes written. Consumers
// call Tail() to include that context in error messages when the agent
// process exits before it emits a structured error — otherwise all the
// user sees is "exit status N", with the real reason stuck in daemon logs.
//
// All backends that supervise a child CLI process should wire their
// cmd.Stderr through this type, and on failure include Tail() in
// Result.Error via withAgentStderr. That makes root-causing CLI crashes
// possible without having to crawl the daemon host's log files.
type stderrTail struct {
inner io.Writer
max int
mu sync.Mutex
buf []byte
}
func newStderrTail(inner io.Writer, max int) *stderrTail {
if max <= 0 {
max = agentStderrTailBytes
}
return &stderrTail{inner: inner, max: max}
}
func (s *stderrTail) Write(p []byte) (int, error) {
if _, err := s.inner.Write(p); err != nil {
return 0, err
}
s.mu.Lock()
s.buf = append(s.buf, p...)
if len(s.buf) > s.max {
s.buf = s.buf[len(s.buf)-s.max:]
}
s.mu.Unlock()
return len(p), nil
}
// Tail returns the captured stderr with leading/trailing whitespace
// trimmed; empty string means nothing was written or everything was
// whitespace.
func (s *stderrTail) Tail() string {
s.mu.Lock()
defer s.mu.Unlock()
return strings.TrimSpace(string(s.buf))
}
// withAgentStderr appends a stderr tail hint to an error message when
// non-empty, otherwise returns msg unchanged. The tail is prefixed with a
// short label so the composed string stays readable even when the original
// msg is already verbose.
func withAgentStderr(msg, label, tail string) string {
if tail == "" {
return msg
}
return msg + "; " + label + " stderr: " + tail
}