fix: prevent JSON event leaking to users and reload skills after AI creation

- OpenCode runtime: stop calling _collect_text fallback on non-text events (step_start, step_finish, etc.)
- Both runtimes: guard raw stdout fallback to only apply for non-JSON output
- main.py: reload skills after AI responses containing skill-related keywords
- main.py: return friendly message instead of empty string for tool-only responses
This commit is contained in:
2026-02-18 23:40:15 -05:00
parent 34dea65a07
commit 4e31e77286
3 changed files with 93 additions and 16 deletions

View File

@@ -695,8 +695,11 @@ class OpenCodeRuntime:
# Parse the output — mirrors OpenClaw's parseCliJson/parseCliJsonl
response_text = self._parse_cli_output(stdout)
if not response_text:
response_text = stdout # fallback to raw output
if not response_text and stdout.strip():
# Only fall back to raw output if it doesn't look like JSON events
# (which would leak internal lifecycle data to the user)
if not stdout.strip().startswith("{"):
response_text = stdout
# Extract session ID if returned
session_id = self._extract_session_id(stdout)
@@ -792,12 +795,17 @@ class OpenCodeRuntime:
{"type":"text", "sessionID":"ses_...", "part":{"type":"text","text":"Hello!"}}
{"type":"step_finish","sessionID":"ses_...", "part":{"type":"step-finish",...}}
We extract text from events where type == "text" and part.text exists.
We extract text ONLY from "text" type events. All other event types
(step_start, step_finish, tool_use, tool_result, etc.) are internal
lifecycle events and must never be shown to the user.
"""
if not stdout.strip():
return ""
# Parse JSONL lines — collect text from "text" type events
# Track whether we found any JSON at all (to distinguish JSONL from plain text)
found_json = False
# Parse JSONL lines — collect text from "text" type events only
lines = stdout.strip().split("\n")
texts = []
for line in lines:
@@ -806,6 +814,7 @@ class OpenCodeRuntime:
continue
try:
event = json.loads(line)
found_json = True
if not isinstance(event, dict):
continue
@@ -819,19 +828,47 @@ class OpenCodeRuntime:
texts.append(text)
continue
# Fallback: try generic text extraction (for non-OpenCode formats)
text = self._collect_text(event)
if text:
texts.append(text)
# Also handle "result" type events (Claude JSON format)
if event_type == "result":
text = event.get("result", "")
if text:
texts.append(text)
continue
# Also handle "assistant" type events (Claude stream-json)
if event_type == "assistant" and "message" in event:
msg = event["message"]
if "content" in msg:
for block in msg["content"]:
if block.get("type") == "text":
t = block.get("text", "")
if t:
texts.append(t)
continue
# Skip all other event types silently (step_start, step_finish,
# tool_use, tool_result, system, etc.)
except json.JSONDecodeError:
# Not JSON — might be plain text output (--format default)
texts.append(line)
# Only include if we haven't seen JSON yet (pure plain text mode)
if not found_json:
texts.append(line)
if texts:
return "\n".join(texts)
# Final fallback to raw text
# If we parsed JSON events but found no text, the response was
# purely tool-use with no user-facing text. Return empty rather
# than leaking raw JSON events.
if found_json:
logger.warning(
"OpenCode output contained only non-text JSON events "
"(no user-facing text found)"
)
return ""
# Final fallback for non-JSON output
return stdout.strip()
def _collect_text(self, value: Any) -> str: