From 41f4e21268f9bc6cbbb5aaba93031bc0ac618107 Mon Sep 17 00:00:00 2001 From: makoMako Date: Fri, 19 Dec 2025 20:50:21 +0800 Subject: [PATCH] fix(gemini): filter noisy stderr output from gemini backend (#83) * fix(gemini): filter noisy stderr output from gemini backend - Add filteringWriter to filter [STARTUP], Warning, Session cleanup etc. - Apply filter only for gemini backend stderr output - Add unit tests for filtering logic * fix: use defer for stderrFilter.Flush to cover all return paths Address review feedback: ensure filter is flushed on failure paths --- codeagent-wrapper/executor.go | 11 ++++- codeagent-wrapper/filter.go | 66 +++++++++++++++++++++++++++++ codeagent-wrapper/filter_test.go | 73 ++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 codeagent-wrapper/filter.go create mode 100644 codeagent-wrapper/filter_test.go diff --git a/codeagent-wrapper/executor.go b/codeagent-wrapper/executor.go index d050a5a..c15b068 100644 --- a/codeagent-wrapper/executor.go +++ b/codeagent-wrapper/executor.go @@ -683,8 +683,17 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe if stderrLogger != nil { stderrWriters = append(stderrWriters, stderrLogger) } + + // For gemini backend, filter noisy stderr output + var stderrFilter *filteringWriter if !silent { - stderrWriters = append([]io.Writer{os.Stderr}, stderrWriters...) + stderrOut := io.Writer(os.Stderr) + if cfg.Backend == "gemini" { + stderrFilter = newFilteringWriter(os.Stderr, geminiNoisePatterns) + stderrOut = stderrFilter + defer stderrFilter.Flush() + } + stderrWriters = append([]io.Writer{stderrOut}, stderrWriters...) } if len(stderrWriters) == 1 { cmd.SetStderr(stderrWriters[0]) diff --git a/codeagent-wrapper/filter.go b/codeagent-wrapper/filter.go new file mode 100644 index 0000000..ab5c522 --- /dev/null +++ b/codeagent-wrapper/filter.go @@ -0,0 +1,66 @@ +package main + +import ( + "bytes" + "io" + "strings" +) + +// geminiNoisePatterns contains stderr patterns to filter for gemini backend +var geminiNoisePatterns = []string{ + "[STARTUP]", + "Session cleanup disabled", + "Warning:", + "(node:", + "(Use `node --trace-warnings", + "Loaded cached credentials", + "Loading extension:", + "YOLO mode is enabled", +} + +// filteringWriter wraps an io.Writer and filters out lines matching patterns +type filteringWriter struct { + w io.Writer + patterns []string + buf bytes.Buffer +} + +func newFilteringWriter(w io.Writer, patterns []string) *filteringWriter { + return &filteringWriter{w: w, patterns: patterns} +} + +func (f *filteringWriter) Write(p []byte) (n int, err error) { + f.buf.Write(p) + for { + line, err := f.buf.ReadString('\n') + if err != nil { + // incomplete line, put it back + f.buf.WriteString(line) + break + } + if !f.shouldFilter(line) { + f.w.Write([]byte(line)) + } + } + return len(p), nil +} + +func (f *filteringWriter) shouldFilter(line string) bool { + for _, pattern := range f.patterns { + if strings.Contains(line, pattern) { + return true + } + } + return false +} + +// Flush writes any remaining buffered content +func (f *filteringWriter) Flush() { + if f.buf.Len() > 0 { + remaining := f.buf.String() + if !f.shouldFilter(remaining) { + f.w.Write([]byte(remaining)) + } + f.buf.Reset() + } +} diff --git a/codeagent-wrapper/filter_test.go b/codeagent-wrapper/filter_test.go new file mode 100644 index 0000000..12042f8 --- /dev/null +++ b/codeagent-wrapper/filter_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestFilteringWriter(t *testing.T) { + tests := []struct { + name string + patterns []string + input string + want string + }{ + { + name: "filter STARTUP lines", + patterns: geminiNoisePatterns, + input: "[STARTUP] Recording metric\nHello World\n[STARTUP] Another line\n", + want: "Hello World\n", + }, + { + name: "filter Warning lines", + patterns: geminiNoisePatterns, + input: "Warning: something bad\nActual output\n", + want: "Actual output\n", + }, + { + name: "filter multiple patterns", + patterns: geminiNoisePatterns, + input: "YOLO mode is enabled\nSession cleanup disabled\nReal content\nLoading extension: foo\n", + want: "Real content\n", + }, + { + name: "no filtering needed", + patterns: geminiNoisePatterns, + input: "Line 1\nLine 2\nLine 3\n", + want: "Line 1\nLine 2\nLine 3\n", + }, + { + name: "empty input", + patterns: geminiNoisePatterns, + input: "", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + fw := newFilteringWriter(&buf, tt.patterns) + fw.Write([]byte(tt.input)) + fw.Flush() + + if got := buf.String(); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestFilteringWriterPartialLines(t *testing.T) { + var buf bytes.Buffer + fw := newFilteringWriter(&buf, geminiNoisePatterns) + + // Write partial line + fw.Write([]byte("Hello ")) + fw.Write([]byte("World\n")) + fw.Flush() + + if got := buf.String(); got != "Hello World\n" { + t.Errorf("got %q, want %q", got, "Hello World\n") + } +}