fix(cli): the paused-chat refusal prints as written; /discard says when another lost turn remains

The refusal behind a lost turn went through the final-response markdown
renderer: `strip` rewrote the quoted prompt (`*bug*`, `__init__.py`,
backticks) and `render` merged the `/discard <id>` line into one
paragraph. A gateway refusal (failure_reason "gateway_connection") is
Hermes's own copy, so the panel now prints it raw.

/discard said "You can send messages again" even when another lost turn
still refused the next message. It now reads the refreshed pending list
and says how many paused turns are left, and uses "Discarded ... it was
not re-run" like messaging's gateway.discard.done (a kept result is not
thrown away).
This commit is contained in:
Teknium committed 2026-10-27 16:46:52 -07:00
1 parent d5f28ed2ec
commit 485f5ce6ce
2 files changed
+67 -4

No files matched your search

+12 -4
View File
@@ -528,11 +528,15 @@ class CLIChatTurnMixin:
lost = lost_admission(connection.reattach(self.session_id), admission_id)
connection.call("prompt.resolve_unknown", session_id=self.session_id,
admission_id=lost["admission_id"], execution_generation=lost["execution_generation"])
connection.reattach(self.session_id) # the cached pending list the next prompt checks
# The cached pending list the next prompt checks: any other lost turn still refuses it.
snapshot = connection.reattach(self.session_id)
except GatewayClientError as exc:
_cprint(f" /discard: {exc}")
return
_cprint(f" Dropped {lost['admission_id']}; it will not be re-run. You can send messages again.")
left = sum(row["status"] == "unknown" for row in snapshot.get("pending", []))
_cprint(f" Discarded {lost['admission_id']}; it was not re-run. "
+ (f"{left} more paused turn{'s' if left > 1 else ''} to /discard first." if left
else "You can send messages again."))
def _chat_render_turn(self, turn, agent_thread, interrupt_msg):
"""Post-turn display: errors, interrupt marker, reasoning/response panels, bell, re-queues.
@@ -724,6 +728,10 @@ class CLIChatTurnMixin:
(self._stream_started and self._stream_box_opened) or _interrupted_streamed
) and not is_error_response
modern = getattr(self, "_modern_layout", False)
# A gateway refusal is Hermes's own copy (quoted user text, a /discard line to copy):
# markdown stripping or rendering would rewrite the quote and merge the lines.
mode = ("raw" if turn.result and turn.result.get("failure_reason") == "gateway_connection"
else self.final_response_markdown)
if turn.use_streaming_tts and turn.box_opened and not is_error_response:
# Text already printed sentence-by-sentence; just close the box.
if modern:
@@ -740,12 +748,12 @@ class CLIChatTurnMixin:
elif modern:
from hermes_cli.cli_modern_layout import answer_label, closing_separator
_cprint(answer_label())
ChatConsole().print(_render_final_assistant_content(response, mode=self.final_response_markdown),
ChatConsole().print(_render_final_assistant_content(response, mode=mode),
style=_resp_text, soft_wrap=True) # terminal wraps; resize re-flows
_cprint(closing_separator(True))
else:
ChatConsole().print(Panel(
_render_final_assistant_content(response, mode=self.final_response_markdown),
_render_final_assistant_content(response, mode=mode),
title=f"[{_resp_color} bold]{label}[/]", title_align="left", border_style=_resp_color,
style=_resp_text, box=rich_box.HORIZONTALS, padding=(1, 0),
width=self._scrollback_box_width(),
@@ -0,0 +1,55 @@
"""The classic CLI's refusal behind a lost turn and its ``/discard`` reply say only what is true."""
from types import SimpleNamespace
import pytest
@pytest.mark.parametrize("mode", ["strip", "render"])
def test_refused_notice_prints_the_users_text_and_discard_line_as_written(monkeypatch, mode):
"""The notice is Hermes's copy, not a model answer: the markdown display modes must neither
rewrite the quoted prompt nor merge the ``/discard`` line into the paragraph around it."""
import cli as cli_mod
from rich.console import Console
from hermes_cli.gateway_chat_view import pending_lines
text = "fix the *bug* in __init__.py and `run` it"
notice = "Your message was not sent.\n" + "\n".join(pending_lines([
{"admission_id": "a1", "status": "unknown", "text": text},
{"admission_id": "q1", "status": "queued", "text": "next"}]))
console = Console(record=True, width=200)
monkeypatch.setattr(cli_mod, "ChatConsole", lambda: console)
stub = SimpleNamespace(final_response_markdown=mode, _last_turn_interrupted=False, _streamed_text_this_turn="",
_stream_started=False, _stream_box_opened=False, _modern_layout=False,
_scrollback_box_width=lambda: 200)
turn = SimpleNamespace(use_streaming_tts=False, box_opened=False, result={
"failed": True, "failure_reason": "gateway_connection", "error": notice, "final_response": ""})
cli_mod.HermesCLI._chat_print_response_panel(stub, turn, notice)
lines = [line.strip() for line in console.export_text().splitlines()]
assert f'"{text}"' in lines and "/discard a1" in lines, lines
def test_discard_says_when_another_lost_turn_still_blocks_the_chat(monkeypatch):
import cli as cli_mod
class Connection:
pending = [{"admission_id": a, "status": "unknown", "execution_generation": 4} for a in ("a1", "a2")]
def reattach(self, _session_id):
return {"pending": list(self.pending)}
def call(self, _method, **params):
self.pending = [row for row in self.pending if row["admission_id"] != params["admission_id"]]
printed = []
monkeypatch.setattr(cli_mod, "_cprint", lambda line: printed.append(line))
stub = SimpleNamespace(session_id="s")
connection = Connection()
monkeypatch.setattr("hermes_cli.gateway_side.connection_for", lambda _cli: connection)
cli_mod.HermesCLI._handle_discard_command(stub, "/discard a1")
cli_mod.HermesCLI._handle_discard_command(stub, "/discard a2")
assert "send messages again" not in printed[0] and "/discard" in printed[0], printed
assert "send messages again" in printed[1], printed