From 365da2d2dfd92bd16f1993c8fefea05508282db8 Mon Sep 17 00:00:00 2001 From: Bartok9 <259807879+Bartok9@users.noreply.github.com> Date: Mon, 18 May 2026 20:54:46 -0700 Subject: [PATCH] fix: 4 small surgical bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvages #23302 by @Bartok9. Four independent one-area fixes: 1. kanban boards delete alias now hard-deletes (not archives) — the alias didn't carry --delete, so getattr(args, 'delete', False) returned False. Detect boards_action=='delete' explicitly. 2. Gateway auto-title failures no longer leak as user-visible warnings — debug-log only since they're not actionable. 3. Background process completion notification snaps truncation to the next newline boundary, prepends a marker when content is dropped. 4. _cprint() schedules the run_in_terminal coroutine via asyncio.ensure_future so output isn't silently dropped from background threads (fixes #23185 Bug A). Skips the double-print fallback that would fire for mock paths. --- cli.py | 23 ++++++++++++++++++----- gateway/run.py | 31 +++++++++++++++++++++++-------- hermes_cli/kanban.py | 7 ++++++- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/cli.py b/cli.py index af3504af24..f04721840c 100644 --- a/cli.py +++ b/cli.py @@ -1835,13 +1835,26 @@ def _cprint(text: str): # prompt, prints, and redraws. Fire-and-forget — if scheduling # fails we fall back to a direct print so the line isn't lost. def _schedule(): + # run_in_terminal() may return either: + # • a coroutine / Future (prompt_toolkit ≥ 3.0) — must be scheduled + # via ensure_future so the coroutine is actually awaited; calling + # it bare would leave it unawaited and silently drop the output + # (fixes #23185 Bug A). + # • None (some mocks / older PT builds) — just call the inner + # function directly since PT already executed it synchronously. + # Do NOT fall back to a bare _pt_print when ensure_future raises, + # because run_in_terminal already invoked the lambda in that case + # (the mock path), which would double-print the line. try: - run_in_terminal(lambda: _pt_print(_PT_ANSI(text))) + import asyncio as _aio + import inspect as _inspect + coro = run_in_terminal(lambda: _pt_print(_PT_ANSI(text))) + if coro is not None and (_inspect.isawaitable(coro) or _inspect.iscoroutine(coro)): + _aio.ensure_future(coro) + # else: run_in_terminal ran the lambda synchronously; nothing more + # to do (double-scheduling would print twice). except Exception: - try: - _pt_print(_PT_ANSI(text)) - except Exception: - pass + pass # best-effort; the line may already have been printed try: loop.call_soon_threadsafe(_schedule) diff --git a/gateway/run.py b/gateway/run.py index 091300af16..91fd7b2623 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -13956,7 +13956,19 @@ class GatewayRunner: from tools.process_registry import process_registry as _pr_check if agent_notify and not _pr_check.is_completion_consumed(session_id): from tools.ansi_strip import strip_ansi - _out = strip_ansi(session.output_buffer[-2000:]) if session.output_buffer else "" + _raw = strip_ansi(session.output_buffer) if session.output_buffer else "" + # Truncate at line boundaries so notifications never start + # mid-line (fixes #23284). Keep the last ~2000 chars but + # snap to the nearest preceding newline, then prepend a + # truncation marker when output was cut. + _LIMIT = 2000 + if len(_raw) > _LIMIT: + _tail = _raw[-_LIMIT:] + _nl = _tail.find("\n") + _tail = _tail[_nl + 1:] if _nl != -1 else _tail + _out = f"[… output truncated — showing last {len(_tail)} chars]\n{_tail}" + else: + _out = _raw synth_text = ( f"[IMPORTANT: Background process {session_id} completed " f"(exit code {session.exit_code}).\n" @@ -16156,13 +16168,16 @@ class GatewayRunner: try: from agent.title_generator import maybe_auto_title all_msgs = result_holder[0].get("messages", []) if result_holder[0] else [] - # Route title-generation failures through the agent's - # user-visible warning channel so a depleted auxiliary - # provider doesn't silently leave sessions untitled - # (issue #15775). - _title_failure_cb = getattr( - agent, "_emit_auxiliary_failure", None - ) + # In Gateway mode, auto-title failures must NOT be + # surfaced as user-visible messages (fixes #23246). + # Log them at debug level only — they are not actionable + # to the end user. CLI mode keeps the existing behaviour + # via the agent's _emit_auxiliary_failure path. + def _title_failure_cb(task: str, exc: BaseException) -> None: + logger.debug( + "Gateway auto-title failure suppressed (not user-visible): %s: %s", + task, exc, + ) maybe_auto_title_kwargs = { "failure_callback": _title_failure_cb, "main_runtime": { diff --git a/hermes_cli/kanban.py b/hermes_cli/kanban.py index e23d036b37..b891a57beb 100644 --- a/hermes_cli/kanban.py +++ b/hermes_cli/kanban.py @@ -951,8 +951,13 @@ def _cmd_boards_create(args: argparse.Namespace) -> int: def _cmd_boards_rm(args: argparse.Namespace) -> int: + # When the user runs `hermes kanban boards delete ` (alias), the + # boards_action is 'delete' but args.delete is never set to True because + # the --delete flag belongs to the 'rm' subparser only. Detect the alias + # and treat it identically to `boards rm --delete` (fixes #23139). + force_delete = getattr(args, "delete", False) or getattr(args, "boards_action", "") == "delete" try: - res = kb.remove_board(args.slug, archive=not getattr(args, "delete", False)) + res = kb.remove_board(args.slug, archive=not force_delete) except ValueError as exc: print(f"kanban boards rm: {exc}", file=sys.stderr) return 1