From c95f74cb7fa23e0726ce44b9a009213cc044386d Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Sat, 11 Apr 2026 23:04:48 +0800 Subject: [PATCH] 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. --- server/pkg/agent/openclaw.go | 29 +++++++++++++++++++---------- server/pkg/agent/openclaw_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/server/pkg/agent/openclaw.go b/server/pkg/agent/openclaw.go index cba339b2b..07bd0f5de 100644 --- a/server/pkg/agent/openclaw.go +++ b/server/pkg/agent/openclaw.go @@ -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) diff --git a/server/pkg/agent/openclaw_test.go b/server/pkg/agent/openclaw_test.go index 523352234..b995208d6 100644 --- a/server/pkg/agent/openclaw_test.go +++ b/server/pkg/agent/openclaw_test.go @@ -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) {