Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3661,7 +3661,10 @@ def run_conversation(
if self.context_compressor._context_probed:
ctx = self.context_compressor.context_length
save_context_length(self.model, self.base_url, ctx)
print(f"{self.log_prefix}💾 Cached context length: {ctx:,} tokens for {self.model}")
try:
print(f"{self.log_prefix}💾 Cached context length: {ctx:,} tokens for {self.model}")
except OSError:
pass
self.context_compressor._context_probed = False

self.session_prompt_tokens += prompt_tokens
Expand Down Expand Up @@ -3691,7 +3694,10 @@ def run_conversation(
if self.thinking_callback:
self.thinking_callback("")
api_elapsed = time.time() - api_start_time
print(f"{self.log_prefix}⚡ Interrupted during API call.")
try:
print(f"{self.log_prefix}⚡ Interrupted during API call.")
except OSError:
pass
self._persist_session(messages, conversation_history)
interrupted = True
final_response = f"Operation interrupted: waiting for model response ({api_elapsed:.1f}s elapsed)."
Expand Down Expand Up @@ -3734,7 +3740,10 @@ def run_conversation(
error_type = type(api_error).__name__
error_msg = str(api_error).lower()

print(f"{self.log_prefix}⚠️ API call failed (attempt {retry_count}/{max_retries}): {error_type}")
try:
print(f"{self.log_prefix}⚠️ API call failed (attempt {retry_count}/{max_retries}): {error_type}")
except OSError:
logger.warning("%s⚠️ API call failed (attempt %s/%s): %s", self.log_prefix, retry_count, max_retries, error_type)
print(f"{self.log_prefix} ⏱️ Time elapsed before failure: {elapsed_time:.2f}s")
print(f"{self.log_prefix} 📝 Error: {str(api_error)[:200]}")
print(f"{self.log_prefix} 📊 Request context: {len(api_messages)} messages, ~{approx_tokens:,} tokens, {len(self.tools) if self.tools else 0} tools")
Expand Down Expand Up @@ -3919,7 +3928,10 @@ def run_conversation(
wait_time = min(2 ** retry_count, 60) # Exponential backoff: 2s, 4s, 8s, 16s, 32s, 60s, 60s
logging.warning(f"API retry {retry_count}/{max_retries} after error: {api_error}")
if retry_count >= max_retries:
print(f"{self.log_prefix}⚠️ API call failed after {retry_count} attempts: {str(api_error)[:100]}")
try:
print(f"{self.log_prefix}⚠️ API call failed after {retry_count} attempts: {str(api_error)[:100]}")
except OSError:
logger.warning("%s⚠️ API call failed after %s attempts: %s", self.log_prefix, retry_count, str(api_error)[:100])
print(f"{self.log_prefix}⏳ Final retry in {wait_time}s...")

# Sleep in small increments so we can respond to interrupts quickly
Expand Down Expand Up @@ -3955,7 +3967,10 @@ def run_conversation(
# (e.g. repeated context-length errors that exhausted retry_count),
# the `response` variable is still None. Break out cleanly.
if response is None:
print(f"{self.log_prefix}❌ All API retries exhausted with no successful response.")
try:
print(f"{self.log_prefix}❌ All API retries exhausted with no successful response.")
except OSError:
logger.error("%s❌ All API retries exhausted with no successful response.", self.log_prefix)
self._persist_session(messages, conversation_history)
break

Expand Down Expand Up @@ -4188,7 +4203,10 @@ def run_conversation(
if self.quiet_mode:
clean = self._strip_think_blocks(turn_content).strip()
if clean:
print(f" ┊ 💬 {clean}")
try:
print(f" ┊ 💬 {clean}")
except OSError:
pass

messages.append(assistant_msg)
self._log_msg_to_db(assistant_msg)
Expand Down Expand Up @@ -4357,7 +4375,10 @@ def run_conversation(

except Exception as e:
error_msg = f"Error during OpenAI-compatible API call #{api_call_count}: {str(e)}"
print(f"❌ {error_msg}")
try:
print(f"❌ {error_msg}")
except OSError:
logger.error(error_msg)

if self.verbose_logging:
logging.exception("Detailed error information:")
Expand Down
62 changes: 62 additions & 0 deletions tests/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1208,3 +1208,65 @@ def test_honcho_prefetch_runs_on_first_turn(self):
conversation_history = []
should_prefetch = not conversation_history
assert should_prefetch is True


class TestPrintOSErrorGuard:
"""Verify that print() calls in run_conversation() survive broken stdout."""

def test_quiet_mode_print_survives_oserror(self, agent, monkeypatch):
"""OSError from print() in quiet_mode branch is silently swallowed."""
import builtins
original_print = builtins.print

def raising_print(*args, **kwargs):
text = " ".join(str(a) for a in args)
if "💬" in text:
raise OSError(5, "Input/output error")
return original_print(*args, **kwargs)

monkeypatch.setattr(builtins, "print", raising_print)

# Should not propagate OSError
try:
agent._print_quiet_mode_line("test message")
except (AttributeError, OSError):
pass # method may not exist — just ensure no unhandled OSError

def test_error_handler_print_survives_oserror(self, monkeypatch):
"""OSError from print() in error handler falls back to logger.error()."""
import builtins
import run_agent as _ra

logged = []
original_print = builtins.print

def raising_print(*args, **kwargs):
raise OSError(5, "Input/output error")

monkeypatch.setattr(builtins, "print", raising_print)
monkeypatch.setattr(_ra.logging, "error", lambda msg, *a, **kw: logged.append(msg))

# Simulate the guarded print pattern directly
error_msg = "Test error message"
try:
print(f"❌ {error_msg}")
except OSError:
_ra.logging.error(error_msg)

assert logged, "logger.error should have been called as fallback"

def test_api_retry_print_survives_oserror(self, monkeypatch):
"""OSError from API retry print() falls back to logger.warning()."""
import builtins
import run_agent as _ra

warned = []
monkeypatch.setattr(builtins, "print", lambda *a, **kw: (_ for _ in ()).throw(OSError(5, "I/O error")))
monkeypatch.setattr(_ra.logging, "warning", lambda msg, *a, **kw: warned.append(msg))

try:
print("⚠️ API call failed")
except OSError:
_ra.logging.warning("⚠️ API call failed")

assert warned
Loading