fix(cli): robust paste file expansion and process_loop error handling (#17666)

Two narrow fixes for long pasted messages silently disappearing:

1. _expand_paste_references: replace path.exists() + read_text() with
   try/except (OSError, IOError). Closes the TOCTOU window where a paste
   file deleted between check and read raised FileNotFoundError, bubbled
   up through process_loop's outer except, and silently dropped the
   user's input. Failures now return the placeholder text and log a
   warning.

2. process_loop outer except: logger.warning() instead of print().
   prompt_toolkit's TUI swallows stdout, so 'Error: …' was invisible
   to the user. Logged errors are discoverable via hermes logs.

Dropped the larger interrupt_queue→pending_input drain that was part of
the original PR — that's a separate class of input-drop (in-progress
interrupt handling) unrelated to the paste-file TOCTOU reported in the
issue, and worth its own review.

Salvage of #17939.
This commit is contained in:
ambition0802 2026-05-02 02:05:57 -07:00 committed by Teknium
parent 5eac6084bc
commit 7696ddc59e

11
cli.py
View File

@ -2928,7 +2928,14 @@ class HermesCLI:
def _expand_ref(match): def _expand_ref(match):
path = Path(match.group(1)) path = Path(match.group(1))
return path.read_text(encoding="utf-8") if path.exists() else match.group(0) # Use try/except instead of path.exists() to avoid TOCTOU race:
# the paste file may be deleted between check and read, causing
# the input to be silently dropped (#17666).
try:
return path.read_text(encoding="utf-8")
except (OSError, IOError):
logger.warning("Paste file gone or unreadable, returning placeholder: %s", path)
return match.group(0)
return paste_ref_re.sub(_expand_ref, text) return paste_ref_re.sub(_expand_ref, text)
@ -11584,7 +11591,7 @@ class HermesCLI:
pass # Non-fatal — don't break the main loop pass # Non-fatal — don't break the main loop
except Exception as e: except Exception as e:
print(f"Error: {e}") logger.warning("process_loop unhandled error (msg may be lost): %s", e)
# Start processing thread # Start processing thread
process_thread = threading.Thread(target=process_loop, daemon=True) process_thread = threading.Thread(target=process_loop, daemon=True)