fix(agent): handle braces in stderr log lines before openclaw JSON result

processOutput() used strings.Index(raw, "{") to find the JSON start,
but error lines like `raw_params={"command":"..."}` contain braces that
get matched first, causing JSON parsing to fail and the entire raw
stderr (including internal metadata) to be returned as the agent comment.

Now tries each '{' position until one successfully unmarshals as a valid
openclawResult, skipping braces embedded in log/error lines.
This commit is contained in:
Jiang Bohan
2026-04-11 23:04:48 +08:00
parent e477d64548
commit c95f74cb7f
2 changed files with 45 additions and 10 deletions

View File

@@ -129,16 +129,31 @@ type openclawEventResult struct {
// processOutput reads the JSON output from openclaw --json stderr and returns
// the parsed result. OpenClaw writes its JSON result to stderr, which may also
// contain non-JSON log lines. We extract the JSON object by finding the first '{'.
// contain non-JSON log lines. We find the result JSON by trying each '{' until
// one successfully unmarshals as an openclawResult with payloads.
func (b *openclawBackend) processOutput(r io.Reader, ch chan<- Message) openclawEventResult {
data, err := io.ReadAll(r)
if err != nil {
return openclawEventResult{status: "failed", errMsg: fmt.Sprintf("read stderr: %v", err)}
}
// Log non-JSON lines and find the JSON object
raw := string(data)
jsonStart := strings.Index(raw, "{")
// Try each '{' position until we find valid openclawResult JSON.
// Earlier '{' chars may appear in log/error lines (e.g. raw_params={...}).
var result openclawResult
jsonStart := -1
for i := 0; i < len(raw); i++ {
if raw[i] != '{' {
continue
}
if err := json.Unmarshal([]byte(raw[i:]), &result); err == nil && result.Payloads != nil {
jsonStart = i
break
}
}
// Log non-JSON lines before the result
if jsonStart > 0 {
for _, line := range strings.Split(raw[:jsonStart], "\n") {
line = strings.TrimSpace(line)
@@ -148,13 +163,7 @@ func (b *openclawBackend) processOutput(r io.Reader, ch chan<- Message) openclaw
}
}
var result openclawResult
jsonData := raw
if jsonStart >= 0 {
jsonData = raw[jsonStart:]
}
if err := json.Unmarshal([]byte(jsonData), &result); err != nil {
// If we can't parse JSON, return raw output as-is
if jsonStart < 0 {
trimmed := strings.TrimSpace(raw)
if trimmed != "" {
b.cfg.Logger.Debug("[openclaw:stderr] " + trimmed)

View File

@@ -203,6 +203,32 @@ func TestOpenclawProcessOutputReadError(t *testing.T) {
close(ch)
}
func TestOpenclawProcessOutputWithBracesInLogLines(t *testing.T) {
t.Parallel()
b := &openclawBackend{cfg: Config{Logger: slog.Default()}}
ch := make(chan Message, 256)
result := openclawResult{
Payloads: []openclawPayload{{Text: "Final answer"}},
Meta: openclawMeta{DurationMs: 500},
}
data, _ := json.Marshal(result)
// Simulate error line containing braces before the real JSON (the exact bug scenario)
input := `[tools] exec failed: complex interpreter invocation detected. raw_params={"command":"echo hello"}` + "\n" + string(data)
res := b.processOutput(strings.NewReader(input), ch)
if res.status != "completed" {
t.Errorf("status: got %q, want %q", res.status, "completed")
}
if res.output != "Final answer" {
t.Errorf("output: got %q, want %q", res.output, "Final answer")
}
close(ch)
}
// ── openclawInt64 tests ──
func TestOpenclawInt64Float(t *testing.T) {