hermes - 💡(How to fix) Fix [Bug]: gateway `(×N)` repeat counter fires for distinct commands that share a long prefix [4 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Additional Logs / Traceback (optional)

Root Cause

Root Cause Analysis (optional)

Fix Action

Fixed

Code Example

Run each of these in /home/agent/Coding/my-project
   as a separate terminal call, no combining:
     git rev-parse --abbrev-ref HEAD
     git log -1 --oneline
     git status -s | wc -l
     git branch --show-current
     cat config/database.yml | head -1

---

💻 terminal: "cd /home/agent/Coding/Greentech-Redmi..." (×5)

---

Report       https://paste.rs/P8iA5
  agent.log    https://paste.rs/8LpTH
  gateway.log  https://paste.rs/fmfBW

---



---

if preview:
    _pl = get_tool_preview_max_len()
    _cap = _pl if _pl > 0 else 40
    if len(preview) > _cap:
        preview = preview[:_cap - 3] + "..."        # ← head-cut, drops the tail
    msg = f'{emoji} {tool_name}: "{preview}"'
if msg == last_progress_msg[0]:                     # ← dedup runs on TRUNCATED msg
    repeat_count[0] += 1
    progress_queue.put(("__dedup__", msg, repeat_count[0]))

---

last_progress_preview = [None]        # untruncated preview from prev tick
last_progress_preview_trunc = [None]  # its truncated form (cached → returned on identity)

---

💻 terminal: "cd /home/agent/Coding/G...bbrev-ref HEAD"
💻 terminal: "cd /home/agent/Coding...log -1 --oneline"
💻 terminal: "cd /home/agent/Codin...status -s | wc -l"
💻 terminal: "cd /home/agent/Codin...nch --show-current"
💻 terminal: "cd /home/age...se.yml | head -1"
RAW_BUFFERClick to expand / collapse

Bug Description

The gateway's tool-progress display dedupes consecutive identical preview lines into a (×N) repeat counter. But it dedupes on the already-truncated preview, not the raw tool argument. The preview is head-cut to ~40 chars (text[:cap-3] + "..."), so any two commands that happen to share the first ~37 chars collapse to the same truncated string — and the dedup then folds them into one bubble with (×N). The user sees what looks like a single tool running N times when in reality each call was a different command.

This isn't a cosmetic issue: it actively misleads both human readers AND any automation that uses (×N) as a loop / stall signal. On orchestrated workflows it causes spurious "stuck delegatee" recoveries; on stall-watchers it triggers false-positive nudges.

Steps to Reproduce

  1. Run Hermes against a gateway (any: Matrix, Telegram, Slack, Discord) with default display.tool_progress: all.

  2. Prompt the agent to issue several distinct shell commands that share a long path prefix. Example:

    Run each of these in /home/agent/Coding/my-project
    as a separate terminal call, no combining:
      git rev-parse --abbrev-ref HEAD
      git log -1 --oneline
      git status -s | wc -l
      git branch --show-current
      cat config/database.yml | head -1
  3. Observe the tool-progress bubbles emitted in the chat.

Expected Behavior

Five distinct bubbles, each preview showing enough of the trailing action to tell them apart. The (×N) counter should only fire when two adjacent tool calls really had the same arguments.

Actual Behavior

A single bubble collapses all five into one:

💻 terminal: "cd /home/agent/Coding/Greentech-Redmi..." (×5)

All five cd … && <different action> calls truncate to the same 40-char prefix → identical msg strings → dedup-by-equality fires → false (×5). The differing action keyword at the tail is exactly what the reader needs to distinguish the calls, but it's the part that was thrown away by the head-cut.

A genuine repeat — same command issued twice in a row by mistake — produces the same visual signature, so the counter no longer means what it claims to mean.

Affected Component

Gateway (Telegram/Discord/Slack/WhatsApp)

Messaging Platform (if gateway-related)

Telegram, Discord, Slack, WhatsApp

Debug Report

Report       https://paste.rs/P8iA5
  agent.log    https://paste.rs/8LpTH
  gateway.log  https://paste.rs/fmfBW

Operating System

Ubuntu 24.04.4 LTS

Python Version

3.11.15

Hermes Version

0.13.0

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

In gateway/run.py inside the progress_callback closure (current main, around line 14310):

if preview:
    _pl = get_tool_preview_max_len()
    _cap = _pl if _pl > 0 else 40
    if len(preview) > _cap:
        preview = preview[:_cap - 3] + "..."        # ← head-cut, drops the tail
    msg = f'{emoji} {tool_name}: "{preview}"'
if msg == last_progress_msg[0]:                     # ← dedup runs on TRUNCATED msg
    repeat_count[0] += 1
    progress_queue.put(("__dedup__", msg, repeat_count[0]))

msg is built after the truncation, then compared. Two raw previews that differ only in their trailing chars collapse to byte-identical msg strings and the (×N) path takes over.

The dedup-by-equality property must be preserved for genuine repeats (the counter has real value when commands really do repeat) — so the fix can't just disable dedup. It has to be in the truncation: same input → same output (genuine repeats still coalesce), different input → different output (distinct commands stay distinct).

Proposed Fix (optional)

A small truncate_middle(text, max_len, prev=None, prev_trunc=None) helper in agent/display.py, replacing the inline head-cut at the single gateway call site. Resolution order:

  1. text == prev and prev_trunc is given → return prev_trunc verbatim. The dedup contract is preserved by construction — identical inputs always produce identical outputs.
  2. text fits in max_len → return text unchanged. There's no reason to hide chars when we have room to display them.
  3. prev is provided → show text from the first differing char through the end (the part the reader needs to see), then prepend as much of the shared prefix as remaining budget allows, with "..." marking the elision.
  4. Otherwise → stateless head + ellipsis + short tail (the current behaviour, kept as a fallback for the no-history case).

Gateway side: two new closure state vars next to last_progress_msg:

last_progress_preview = [None]        # untruncated preview from prev tick
last_progress_preview_trunc = [None]  # its truncated form (cached → returned on identity)

The (×N) dedup block below stays unchanged because identical inputs still produce byte-identical msg strings.

Reproduction sequence after the fix, with the same 40-char budget:

💻 terminal: "cd /home/agent/Coding/G...bbrev-ref HEAD"
💻 terminal: "cd /home/agent/Coding...log -1 --oneline"
💻 terminal: "cd /home/agent/Codin...status -s | wc -l"
💻 terminal: "cd /home/agent/Codin...nch --show-current"
💻 terminal: "cd /home/age...se.yml | head -1"

Genuine repeats still collapse: when the same command fires twice the function returns the cached prev_trunc, the two msg strings are byte-identical, and (×N) fires correctly.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING