fix: guard json.loads() against invalid TTS and skill_view responses

Two code paths call json.loads() on output from external tools without
catching JSONDecodeError. If the tool returns a non-JSON string (error
message, empty string, or None), the entire call path crashes.

1. gateway/run.py — text_to_speech_tool() result in voice reply path.
   A TTS failure that returns an error string instead of JSON crashes
   the voice reply handler, killing the message response entirely.

2. cron/scheduler.py — skill_view() result when loading skills for
   cron jobs. A corrupted or missing skill file that returns an error
   string instead of JSON crashes the cron tick, preventing all jobs
   from executing that cycle.

Both fixes catch (json.JSONDecodeError, TypeError), log a warning,
and gracefully skip the failed operation instead of crashing.
This commit is contained in:
vanthinh6886 2026-05-18 20:03:38 -07:00 committed by Teknium
parent 5987b24314
commit 2b538c1f4e
2 changed files with 11 additions and 2 deletions

View file

@ -1034,7 +1034,12 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:
parts = []
skipped: list[str] = []
for skill_name in skill_names:
loaded = json.loads(skill_view(skill_name))
try:
loaded = json.loads(skill_view(skill_name))
except (json.JSONDecodeError, TypeError):
logger.warning("Cron job '%s': skill '%s' returned invalid JSON, skipping", job.get("name", job.get("id")), skill_name)
skipped.append(skill_name)
continue
if not loaded.get("success"):
error = loaded.get("error") or f"Failed to load skill '{skill_name}'"
logger.warning("Cron job '%s': skill not found, skipping — %s", job.get("name", job.get("id")), error)

View file

@ -10592,7 +10592,11 @@ class GatewayRunner:
result_json = await asyncio.to_thread(
text_to_speech_tool, text=tts_text, output_path=audio_path
)
result = json.loads(result_json)
try:
result = json.loads(result_json)
except (json.JSONDecodeError, TypeError):
logger.warning("Auto voice reply TTS returned invalid JSON: %s", result_json[:200] if result_json else result_json)
return
# Use the actual file path from result (may differ after opus conversion)
actual_path = result.get("file_path", audio_path)