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

@@ -259,8 +259,11 @@ class ClaudeCodeRuntime:
# Parse the output
response_text, session_id, usage = self._parse_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
# Store session mapping
if session_id and conversation_id:
@@ -420,7 +423,8 @@ class ClaudeCodeRuntime:
try:
event = json.loads(line)
if isinstance(event, dict):
if event.get("type") == "result":
event_type = event.get("type", "")
if event_type == "result":
text_parts.append(event.get("result", ""))
session_id = event.get("session_id", session_id)
usage = {
@@ -430,7 +434,7 @@ class ClaudeCodeRuntime:
"duration_api_ms": event.get("duration_api_ms", 0),
"is_error": event.get("is_error", False),
}
elif event.get("type") == "assistant" and "message" in event:
elif event_type == "assistant" and "message" in event:
# Extract text from content blocks
msg = event["message"]
if "content" in msg:
@@ -438,14 +442,33 @@ class ClaudeCodeRuntime:
if block.get("type") == "text":
text_parts.append(block.get("text", ""))
session_id = event.get("session_id", session_id)
# Silently skip non-content events (step_start, step_finish,
# system, tool_use, tool_result, etc.) — these are internal
# lifecycle events that should never reach the user.
except json.JSONDecodeError:
# Not JSON — could be plain text mixed in; only include if
# it doesn't look like a truncated JSON blob.
if not line.startswith("{") and not line.startswith("["):
text_parts.append(line)
continue
if text_parts:
return "\n".join(text_parts), session_id, usage
# Fallback: treat as plain text
return stdout, None, None
# Fallback: treat as plain text, but strip any JSON-like lines
# to prevent raw event objects from leaking to the user.
plain_lines = []
for line in stdout.splitlines():
stripped = line.strip()
if stripped and not stripped.startswith("{"):
plain_lines.append(line)
if plain_lines:
return "\n".join(plain_lines), None, None
# Everything was JSON events with no extractable text
logger.warning("Claude output contained only non-content JSON events")
return "", None, None
# -------------------------------------------------------------------
# Validation