mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-05 07:41:39 +00:00
fix: 4 small surgical bugs
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.
This commit is contained in:
parent
3a7ed7be08
commit
365da2d2df
3 changed files with 47 additions and 14 deletions
23
cli.py
23
cli.py
|
|
@ -1835,13 +1835,26 @@ def _cprint(text: str):
|
||||||
# prompt, prints, and redraws. Fire-and-forget — if scheduling
|
# prompt, prints, and redraws. Fire-and-forget — if scheduling
|
||||||
# fails we fall back to a direct print so the line isn't lost.
|
# fails we fall back to a direct print so the line isn't lost.
|
||||||
def _schedule():
|
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:
|
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:
|
except Exception:
|
||||||
try:
|
pass # best-effort; the line may already have been printed
|
||||||
_pt_print(_PT_ANSI(text))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.call_soon_threadsafe(_schedule)
|
loop.call_soon_threadsafe(_schedule)
|
||||||
|
|
|
||||||
|
|
@ -13956,7 +13956,19 @@ class GatewayRunner:
|
||||||
from tools.process_registry import process_registry as _pr_check
|
from tools.process_registry import process_registry as _pr_check
|
||||||
if agent_notify and not _pr_check.is_completion_consumed(session_id):
|
if agent_notify and not _pr_check.is_completion_consumed(session_id):
|
||||||
from tools.ansi_strip import strip_ansi
|
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 = (
|
synth_text = (
|
||||||
f"[IMPORTANT: Background process {session_id} completed "
|
f"[IMPORTANT: Background process {session_id} completed "
|
||||||
f"(exit code {session.exit_code}).\n"
|
f"(exit code {session.exit_code}).\n"
|
||||||
|
|
@ -16156,13 +16168,16 @@ class GatewayRunner:
|
||||||
try:
|
try:
|
||||||
from agent.title_generator import maybe_auto_title
|
from agent.title_generator import maybe_auto_title
|
||||||
all_msgs = result_holder[0].get("messages", []) if result_holder[0] else []
|
all_msgs = result_holder[0].get("messages", []) if result_holder[0] else []
|
||||||
# Route title-generation failures through the agent's
|
# In Gateway mode, auto-title failures must NOT be
|
||||||
# user-visible warning channel so a depleted auxiliary
|
# surfaced as user-visible messages (fixes #23246).
|
||||||
# provider doesn't silently leave sessions untitled
|
# Log them at debug level only — they are not actionable
|
||||||
# (issue #15775).
|
# to the end user. CLI mode keeps the existing behaviour
|
||||||
_title_failure_cb = getattr(
|
# via the agent's _emit_auxiliary_failure path.
|
||||||
agent, "_emit_auxiliary_failure", None
|
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 = {
|
maybe_auto_title_kwargs = {
|
||||||
"failure_callback": _title_failure_cb,
|
"failure_callback": _title_failure_cb,
|
||||||
"main_runtime": {
|
"main_runtime": {
|
||||||
|
|
|
||||||
|
|
@ -951,8 +951,13 @@ def _cmd_boards_create(args: argparse.Namespace) -> int:
|
||||||
|
|
||||||
|
|
||||||
def _cmd_boards_rm(args: argparse.Namespace) -> int:
|
def _cmd_boards_rm(args: argparse.Namespace) -> int:
|
||||||
|
# When the user runs `hermes kanban boards delete <slug>` (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:
|
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:
|
except ValueError as exc:
|
||||||
print(f"kanban boards rm: {exc}", file=sys.stderr)
|
print(f"kanban boards rm: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue