System error messages appearing with wrong role assignment

NousResearch/hermes-agent · cluster 2228 · agent

Data as of

Nothing open on GitHub. Fix merged in #2303. Check it covers your case.

The short answer

0open issues
0open PRs
6items grouped
Known?
Yes. 6 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #2303. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and fixes concern a bug where system-generated error messages were being injected with a dynamic or incorrect role (appearing as role=user instead of role=assistant). The cluster focuses on correcting role assignment for system error messages and related session resume behavior.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #2303 fix: remove synthetic error message injection, fix session resume after repeated failures

    merged to main

    files: hermes_cli/plugins.py, run_agent.py, tests/test_plugins.py, website/docs/reference/environment-variables.md, website/docs/user-guide/features/plugins.md

    Show the diff
    • hermes_cli/plugins.py
      @@ -5,7 +5,8 @@
       Discovers, loads, and manages plugins from three sources:
       
       1. **User plugins**   – ``~/.hermes/plugins/<name>/``
      -2. **Project plugins** – ``./.hermes/plugins/<name>/``
      +2. **Project plugins** – ``./.hermes/plugins/<name>/`` (opt-in via
      +   ``HERMES_ENABLE_PROJECT_PLUGINS``)
       3. **Pip plugins**     – packages that expose the ``hermes_agent.plugins``
          entry-point group.
       
      @@ -62,6 +63,11 @@
       _NS_PARENT = "hermes_plugins"
       
       
      +def _env_enabled(name: str) -> bool:
      +    """Return True when an env var is set to a truthy opt-in value."""
      +    return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
      +
      +
       # ---------------------------------------------------------------------------
       # Data classes
       # ---------------------------------------------------------------------------
      @@ -186,8 +192,9 @@ def discover_and_load(self) -> None:
               manifests.extend(self._scan_directory(user_dir, source="user"))
       
               # 2. Project plugins (./.hermes/plugins/)
      -        project_dir = Path.cwd() / ".hermes" / "plugins"
      -        manifests.extend(self._scan_directory(project_dir, source="project"))
      +        if _env_enabled("HERMES_ENABLE_PROJECT_PLUGINS"):
      +            project_dir = Path.cwd() / ".hermes" / "plugins"
      +            manifests.extend(self._scan_directory(project_dir, source="project"))
       
               # 3. Pip / entry-point plugins
               manifests.extend(self._scan_entry_points())
    • run_agent.py
      @@ -6807,20 +6807,18 @@ def _stop_spinner():
                               pending_handled = True
                           break
                       
      -                if not pending_handled:
      -                    # Error happened before tool processing (e.g. response parsing).
      -                    # Choose role to avoid consecutive same-role messages.
      -                    last_role = messages[-1].get("role") if messages else None
      -                    err_role = "assistant" if last_role == "user" else "user"
      -                    sys_err_msg = {
      -                        "role": err_role,
      -                        "content": f"[System error during processing: {error_msg}]",
      -                    }
      -                    messages.append(sys_err_msg)
      -                
      +                # Non-tool errors don't need a synthetic message injected.
      +                # The error is already printed to the user (line above), and
      +                # the retry loop continues.  Injecting a fake user/assistant
      +                # message pollutes history, burns tokens, and risks violating
      +                # role-alternation invariants.
      +
                       # If we're near the limit, break to avoid infinite loops
                       if api_call_count >= self.max_iterations - 1:
                           final_response = f"I apologize, but I encountered repeated errors: {error_msg}"
      +                    # Append as assistant so the history stays valid for
      +                    # session resume (avoids consecutive user messages).
      +                    messages.append({"role": "assistant", "content": final_response})
                           break
               
               if final_response is None and (
    • tests/test_plugins.py
      @@ -67,6 +67,7 @@ def test_discover_project_plugins(self, tmp_path, monkeypatch):
               project_dir = tmp_path / "project"
               project_dir.mkdir()
               monkeypatch.chdir(project_dir)
      +        monkeypatch.setenv("HERMES_ENABLE_PROJECT_PLUGINS", "true")
               plugins_dir = project_dir / ".hermes" / "plugins"
               _make_plugin_dir(plugins_dir, "proj_plugin")
       
      @@ -76,6 +77,19 @@ def test_discover_project_plugins(self, tmp_path, monkeypatch):
               assert "proj_plugin" in mgr._plugins
               assert mgr._plugins["proj_plugin"].enabled
       
      +    def test_discover_project_plugins_skipped_by_default(self, tmp_path, monkeypatch):
      +        """Project plugins are not discovered unless explicitly enabled."""
      +        project_dir = tmp_path / "project"
      +        project_dir.mkdir()
      +        monkeypatch.chdir(project_dir)
      +        plugins_dir = project_dir / ".hermes" / "plugins"
      +        _make_plugin_dir(plugins_dir, "proj_plugin")
      +
      +        mgr = PluginManager()
      +        mgr.discover_and_load()
      +
      +        assert "proj_plugin" not in mgr._plugins
      +
           def test_discover_is_idempotent(self, tmp_path, monkeypatch):
               """Calling discover_and_load() twice does not duplicate plugins."""
               plugins_dir = tmp_path / "hermes_test" / "plugins"
    • website/docs/reference/environment-variables.md
      @@ -232,6 +232,7 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe
       | `HERMES_QUIET` | Suppress non-essential output (`true`/`false`) |
       | `HERMES_API_TIMEOUT` | LLM API call timeout in seconds (default: `900`) |
       | `HERMES_EXEC_ASK` | Enable execution approval prompts in gateway mode (`true`/`false`) |
      +| `HERMES_ENABLE_PROJECT_PLUGINS` | Enable auto-discovery of repo-local plugins from `./.hermes/plugins/` (`true`/`false`, default: `false`) |
       | `HERMES_BACKGROUND_NOTIFICATIONS` | Background process notification mode in gateway: `all` (default), `result`, `error`, `off` |
       | `HERMES_EPHEMERAL_SYSTEM_PROMPT` | Ephemeral system prompt injected at API-call time (never persisted to sessions) |
       
    • website/docs/user-guide/features/plugins.md
      @@ -22,6 +22,8 @@ Drop a directory into `~/.hermes/plugins/` with a `plugin.yaml` and Python code:
       
       Start Hermes — your tools appear alongside built-in tools. The model can call them immediately.
       
      +Project-local plugins under `./.hermes/plugins/` are disabled by default. Enable them only for trusted repositories by setting `HERMES_ENABLE_PROJECT_PLUGINS=true` before starting Hermes.
      +
       ## What plugins can do
       
       | Capability | How |
      @@ -38,7 +40,7 @@ Start Hermes — your tools appear alongside built-in tools. The model can call
       | Source | Path | Use case |
       |--------|------|----------|
       | User | `~/.hermes/plugins/` | Personal plugins |
      -| Project | `.hermes/plugins/` | Project-specific plugins |
      +| Project | `.hermes/plugins/` | Project-specific plugins (requires `HERMES_ENABLE_PROJECT_PLUGINS=true`) |
       | pip | `hermes_agent.plugins` entry_points | Distributed packages |
       
       ## Available hooks

4 PRs closed without merging

From the close comments: closed without comment 2 · done elsewhere 2

  • #2231 fix: always use role=assistant for system error messages · done elsewhere → #2303
  • #2236 fix(agent): always use role=assistant for system error messages, never role=user · closed without comment
  • #2253 fix: always use assistant role for system error messages · done elsewhere → #2303
  • #2263 fix: bug: system error messages injected with dynamic role, can appear · closed without comment
Closed issues · 1
  • Closed#2228bug: system error messages injected with dynamic role, can appear as role=user
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#2303
Closed issues
1
Attempts
4 PRs closed without merging
Fix link checked
21 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.