llm: catch all exceptions, validate response, show transcription preview (v1.9.9)
- llm.py: catch OSError/TimeoutError (socket.timeout is NOT a URLError subclass) and bare Exception so no path kills the background thread silently - daemon.py: catch unexpected exceptions from llm.chat and surface them as overlay errors instead of silent thread death - daemon.py: reject LLM responses that are >80 % whitespace (cold-start model returning spaces/blank lines gets discarded, not typed) - daemon.py: show transcribed text in overlay immediately before thinking animation so user can see what was heard while LLM processes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
749ef78383
commit
0875fe7ac0
@ -510,6 +510,9 @@ class Daemon:
|
|||||||
return
|
return
|
||||||
self._emit("busy", label, "Rewriting…")
|
self._emit("busy", label, "Rewriting…")
|
||||||
self._dnotify(f"⌛ {label}", "Rewriting…")
|
self._dnotify(f"⌛ {label}", "Rewriting…")
|
||||||
|
# Show the transcribed text immediately so the user sees what was heard.
|
||||||
|
if self.text_cb:
|
||||||
|
self.text_cb(f"📝 {text}")
|
||||||
# Stream the rewrite into the overlay so you watch the model write
|
# Stream the rewrite into the overlay so you watch the model write
|
||||||
# (the bubble updates token-by-token). The delivered text is still
|
# (the bubble updates token-by-token). The delivered text is still
|
||||||
# the complete result, typed once the rewrite finishes.
|
# the complete result, typed once the rewrite finishes.
|
||||||
@ -559,6 +562,27 @@ class Daemon:
|
|||||||
self._emit("error", label, str(exc))
|
self._emit("error", label, str(exc))
|
||||||
self._dnotify("Rewrite failed", str(exc), "critical")
|
self._dnotify("Rewrite failed", str(exc), "critical")
|
||||||
log(f"ERROR ({label} rewrite): {exc}")
|
log(f"ERROR ({label} rewrite): {exc}")
|
||||||
|
if self.text_cb:
|
||||||
|
self.text_cb(f"✗ {exc}")
|
||||||
|
return
|
||||||
|
except Exception as exc: # noqa: BLE001 - guard against any uncaught error
|
||||||
|
msg = f"LLM error: {exc}"
|
||||||
|
self._emit("error", label, msg)
|
||||||
|
log(f"ERROR ({label} rewrite unexpected): {exc}")
|
||||||
|
if self.text_cb:
|
||||||
|
self.text_cb(f"✗ {msg}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sanity-check: reject responses that are >80 % whitespace —
|
||||||
|
# a model that's cold-starting or misconfigured sometimes streams
|
||||||
|
# spaces or blank lines instead of real output.
|
||||||
|
non_ws = sum(1 for c in text if not c.isspace())
|
||||||
|
if non_ws < max(1, len(text) * 0.20):
|
||||||
|
msg = "LLM returned mostly whitespace — discarded"
|
||||||
|
self._emit("error", label, msg)
|
||||||
|
log(f"ERROR ({label}): {msg} (len={len(text)})")
|
||||||
|
if self.text_cb:
|
||||||
|
self.text_cb(f"✗ {msg}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
|
|||||||
@ -93,6 +93,13 @@ def chat(
|
|||||||
raise LLMError(f"Connection failed: {exc.reason}") from exc
|
raise LLMError(f"Connection failed: {exc.reason}") from exc
|
||||||
except (KeyError, IndexError, TypeError) as exc:
|
except (KeyError, IndexError, TypeError) as exc:
|
||||||
raise LLMError(f"Unexpected response: {exc}") from exc
|
raise LLMError(f"Unexpected response: {exc}") from exc
|
||||||
|
except (TimeoutError, OSError) as exc:
|
||||||
|
# socket.timeout (subclass of OSError / TimeoutError) fires when the server
|
||||||
|
# stops sending data mid-stream. Not wrapped in URLError — must be caught
|
||||||
|
# separately or it would propagate uncaught and kill the background thread.
|
||||||
|
raise LLMError(f"Request timed out or connection lost: {exc}") from exc
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
raise LLMError(f"Unexpected error: {exc}") from exc
|
||||||
|
|
||||||
content = (content or "").strip()
|
content = (content or "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user