mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
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.
73 lines
2.2 KiB
Go
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
|
|
}
|