diff --git a/server/pkg/agent/openclaw.go b/server/pkg/agent/openclaw.go index 7a704119..96c240d8 100644 --- a/server/pkg/agent/openclaw.go +++ b/server/pkg/agent/openclaw.go @@ -152,12 +152,20 @@ func (b *openclawBackend) processEvents(r io.Reader, ch chan<- Message) openclaw case "tool_call": b.handleOCToolCallEvent(event, ch) case "error": + // NOTE: error events unconditionally set finalStatus to "failed" and + // it stays sticky — subsequent text or result events won't revert it. + // This is intentional: once an error fires, the session is considered + // failed regardless of later events. b.handleOCErrorEvent(event, ch, &finalStatus, &finalError) case "step_start": trySend(ch, Message{Type: MessageStatus, Status: "running"}) case "step_end": // Captures final session ID from step_end if present. case "result": + // The result event only updates status on explicit failure. A + // "completed" result is a no-op because finalStatus defaults to + // "completed". Any unrecognized status (e.g. "partial") is also + // treated as success — update this if OpenClaw adds new statuses. if event.Data != nil { if s, ok := event.Data["status"].(string); ok && s != "" { if s == "error" || s == "failed" { @@ -189,7 +197,7 @@ func (b *openclawBackend) processEvents(r io.Reader, ch chan<- Message) openclaw } func (b *openclawBackend) handleOCTextEvent(event openclawEvent, ch chan<- Message, output *strings.Builder) { - text := extractEventText(event.Data) + text := openclawExtractText(event.Data) if text != "" { output.WriteString(text) trySend(ch, Message{Type: MessageText, Content: text}) @@ -197,7 +205,7 @@ func (b *openclawBackend) handleOCTextEvent(event openclawEvent, ch chan<- Messa } func (b *openclawBackend) handleOCThinkingEvent(event openclawEvent, ch chan<- Message) { - text := extractEventText(event.Data) + text := openclawExtractText(event.Data) if text != "" { trySend(ch, Message{Type: MessageThinking, Content: text}) } @@ -233,7 +241,7 @@ func (b *openclawBackend) handleOCToolCallEvent(event openclawEvent, ch chan<- M // If the tool has completed, also emit a tool-result message. status, _ := event.Data["status"].(string) if status == "completed" { - outputStr := extractOCToolOutput(event.Data["output"]) + outputStr := extractToolOutput(event.Data["output"]) trySend(ch, Message{ Type: MessageToolResult, Tool: name, @@ -266,8 +274,9 @@ func (b *openclawBackend) handleOCErrorEvent(event openclawEvent, ch chan<- Mess *finalError = errMsg } -// extractEventText extracts text content from an event data map. -func extractEventText(data map[string]any) string { +// openclawExtractText extracts text content from an openclaw event data map. +// Supports both flat {"text": "..."} and nested {"content": {"text": "..."}} layouts. +func openclawExtractText(data map[string]any) string { if data == nil { return "" } @@ -284,18 +293,6 @@ func extractEventText(data map[string]any) string { return "" } -// extractOCToolOutput converts tool output (string or structured) into a string. -func extractOCToolOutput(output any) string { - if output == nil { - return "" - } - if s, ok := output.(string); ok { - return s - } - data, _ := json.Marshal(output) - return string(data) -} - // ── JSON types for `openclaw agent --output-format stream-json` stdout events ── // openclawEvent represents a single NDJSON line from OpenClaw's stream-json output. diff --git a/server/pkg/agent/openclaw_test.go b/server/pkg/agent/openclaw_test.go index c55c7abe..3e3a6c38 100644 --- a/server/pkg/agent/openclaw_test.go +++ b/server/pkg/agent/openclaw_test.go @@ -517,12 +517,12 @@ func TestOpenclawProcessEventsResultErrorStatus(t *testing.T) { close(ch) } -// ── extractEventText tests ── +// ── openclawExtractText tests ── func TestExtractEventTextDirect(t *testing.T) { t.Parallel() data := map[string]any{"text": "hello"} - if got := extractEventText(data); got != "hello" { + if got := openclawExtractText(data); got != "hello" { t.Errorf("got %q, want %q", got, "hello") } } @@ -532,43 +532,18 @@ func TestExtractEventTextNested(t *testing.T) { data := map[string]any{ "content": map[string]any{"text": "nested hello"}, } - if got := extractEventText(data); got != "nested hello" { + if got := openclawExtractText(data); got != "nested hello" { t.Errorf("got %q, want %q", got, "nested hello") } } func TestExtractEventTextNil(t *testing.T) { t.Parallel() - if got := extractEventText(nil); got != "" { + if got := openclawExtractText(nil); got != "" { t.Errorf("got %q, want empty", got) } } -// ── extractOCToolOutput tests ── - -func TestExtractOCToolOutputString(t *testing.T) { - t.Parallel() - if got := extractOCToolOutput("hello\n"); got != "hello\n" { - t.Errorf("got %q, want %q", got, "hello\n") - } -} - -func TestExtractOCToolOutputNil(t *testing.T) { - t.Parallel() - if got := extractOCToolOutput(nil); got != "" { - t.Errorf("got %q, want empty", got) - } -} - -func TestExtractOCToolOutputStructured(t *testing.T) { - t.Parallel() - obj := map[string]any{"key": "value"} - got := extractOCToolOutput(obj) - if !strings.Contains(got, `"key"`) || !strings.Contains(got, `"value"`) { - t.Errorf("got %q, expected JSON containing key/value", got) - } -} - // ── Thinking event with nested content ── func TestOpenclawHandleThinkingEventNestedContent(t *testing.T) {