fix: batch of 5 small contributor fixes (#2466)
fix: batch of 5 small contributor fixes — PortAudio, SafeWriter, IMAP, thread lock, prefill
This commit is contained in:
commit
edda0e324b
4 changed files with 48 additions and 23 deletions
|
|
@ -230,7 +230,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
# Mark all existing messages as seen so we only process new ones
|
# Mark all existing messages as seen so we only process new ones
|
||||||
imap.select("INBOX")
|
imap.select("INBOX")
|
||||||
status, data = imap.uid("search", None, "ALL")
|
status, data = imap.uid("search", None, "ALL")
|
||||||
if status == "OK" and data[0]:
|
if status == "OK" and data and data[0]:
|
||||||
for uid in data[0].split():
|
for uid in data[0].split():
|
||||||
self._seen_uids.add(uid)
|
self._seen_uids.add(uid)
|
||||||
imap.logout()
|
imap.logout()
|
||||||
|
|
@ -295,7 +295,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
imap.select("INBOX")
|
imap.select("INBOX")
|
||||||
|
|
||||||
status, data = imap.uid("search", None, "UNSEEN")
|
status, data = imap.uid("search", None, "UNSEEN")
|
||||||
if status != "OK" or not data[0]:
|
if status != "OK" or not data or not data[0]:
|
||||||
imap.logout()
|
imap.logout()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -855,23 +855,25 @@ class SessionDB:
|
||||||
|
|
||||||
def session_count(self, source: str = None) -> int:
|
def session_count(self, source: str = None) -> int:
|
||||||
"""Count sessions, optionally filtered by source."""
|
"""Count sessions, optionally filtered by source."""
|
||||||
if source:
|
with self._lock:
|
||||||
cursor = self._conn.execute(
|
if source:
|
||||||
"SELECT COUNT(*) FROM sessions WHERE source = ?", (source,)
|
cursor = self._conn.execute(
|
||||||
)
|
"SELECT COUNT(*) FROM sessions WHERE source = ?", (source,)
|
||||||
else:
|
)
|
||||||
cursor = self._conn.execute("SELECT COUNT(*) FROM sessions")
|
else:
|
||||||
return cursor.fetchone()[0]
|
cursor = self._conn.execute("SELECT COUNT(*) FROM sessions")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
def message_count(self, session_id: str = None) -> int:
|
def message_count(self, session_id: str = None) -> int:
|
||||||
"""Count messages, optionally for a specific session."""
|
"""Count messages, optionally for a specific session."""
|
||||||
if session_id:
|
with self._lock:
|
||||||
cursor = self._conn.execute(
|
if session_id:
|
||||||
"SELECT COUNT(*) FROM messages WHERE session_id = ?", (session_id,)
|
cursor = self._conn.execute(
|
||||||
)
|
"SELECT COUNT(*) FROM messages WHERE session_id = ?", (session_id,)
|
||||||
else:
|
)
|
||||||
cursor = self._conn.execute("SELECT COUNT(*) FROM messages")
|
else:
|
||||||
return cursor.fetchone()[0]
|
cursor = self._conn.execute("SELECT COUNT(*) FROM messages")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Export and cleanup
|
# Export and cleanup
|
||||||
|
|
|
||||||
28
run_agent.py
28
run_agent.py
|
|
@ -108,7 +108,7 @@ HONCHO_TOOL_NAMES = {
|
||||||
|
|
||||||
|
|
||||||
class _SafeWriter:
|
class _SafeWriter:
|
||||||
"""Transparent stdio wrapper that catches OSError from broken pipes.
|
"""Transparent stdio wrapper that catches OSError/ValueError from broken pipes.
|
||||||
|
|
||||||
When hermes-agent runs as a systemd service, Docker container, or headless
|
When hermes-agent runs as a systemd service, Docker container, or headless
|
||||||
daemon, the stdout/stderr pipe can become unavailable (idle timeout, buffer
|
daemon, the stdout/stderr pipe can become unavailable (idle timeout, buffer
|
||||||
|
|
@ -117,8 +117,13 @@ class _SafeWriter:
|
||||||
run_conversation() — especially via double-fault when an except handler
|
run_conversation() — especially via double-fault when an except handler
|
||||||
also tries to print.
|
also tries to print.
|
||||||
|
|
||||||
|
Additionally, when subagents run in ThreadPoolExecutor threads, the shared
|
||||||
|
stdout handle can close between thread teardown and cleanup, raising
|
||||||
|
``ValueError: I/O operation on closed file`` instead of OSError.
|
||||||
|
|
||||||
This wrapper delegates all writes to the underlying stream and silently
|
This wrapper delegates all writes to the underlying stream and silently
|
||||||
catches OSError. It is transparent when the wrapped stream is healthy.
|
catches both OSError and ValueError. It is transparent when the wrapped
|
||||||
|
stream is healthy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_inner",)
|
__slots__ = ("_inner",)
|
||||||
|
|
@ -129,13 +134,13 @@ class _SafeWriter:
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
try:
|
try:
|
||||||
return self._inner.write(data)
|
return self._inner.write(data)
|
||||||
except OSError:
|
except (OSError, ValueError):
|
||||||
return len(data) if isinstance(data, str) else 0
|
return len(data) if isinstance(data, str) else 0
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
try:
|
try:
|
||||||
self._inner.flush()
|
self._inner.flush()
|
||||||
except OSError:
|
except (OSError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def fileno(self):
|
def fileno(self):
|
||||||
|
|
@ -144,7 +149,7 @@ class _SafeWriter:
|
||||||
def isatty(self):
|
def isatty(self):
|
||||||
try:
|
try:
|
||||||
return self._inner.isatty()
|
return self._inner.isatty()
|
||||||
except OSError:
|
except (OSError, ValueError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
|
@ -2438,7 +2443,18 @@ class AIAgent:
|
||||||
"Pre-call sanitizer: added %d stub tool result(s)",
|
"Pre-call sanitizer: added %d stub tool result(s)",
|
||||||
len(missing_results),
|
len(missing_results),
|
||||||
)
|
)
|
||||||
|
# 3. Strip trailing empty assistant messages to prevent prefill rejection.
|
||||||
|
# These can leak from Responses API reasoning-only turns (Codex/MiniMax)
|
||||||
|
# where an empty assistant message is required by the Responses API but
|
||||||
|
# must NOT be sent to Chat Completions or Anthropic Messages API providers.
|
||||||
|
while (
|
||||||
|
messages
|
||||||
|
and messages[-1].get("role") == "assistant"
|
||||||
|
and not (messages[-1].get("content") or "").strip()
|
||||||
|
and not messages[-1].get("tool_calls")
|
||||||
|
):
|
||||||
|
logger.debug("Pre-call sanitizer: removed trailing empty assistant message")
|
||||||
|
messages = messages[:-1]
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,15 @@ def detect_audio_environment() -> dict:
|
||||||
warnings.append("No audio input/output devices detected")
|
warnings.append("No audio input/output devices detected")
|
||||||
except Exception:
|
except Exception:
|
||||||
warnings.append("Audio subsystem error (PortAudio cannot query devices)")
|
warnings.append("Audio subsystem error (PortAudio cannot query devices)")
|
||||||
except (ImportError, OSError):
|
except ImportError:
|
||||||
warnings.append("Audio libraries not installed (pip install sounddevice numpy)")
|
warnings.append("Audio libraries not installed (pip install sounddevice numpy)")
|
||||||
|
except OSError:
|
||||||
|
warnings.append(
|
||||||
|
"PortAudio system library not found -- install it first:\n"
|
||||||
|
" Linux: sudo apt-get install libportaudio2\n"
|
||||||
|
" macOS: brew install portaudio\n"
|
||||||
|
"Then retry /voice on."
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available": len(warnings) == 0,
|
"available": len(warnings) == 0,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue