# NousResearch/hermes-agent · System error messages appearing with wrong role assignment

## Open now

- 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.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #2303. Verify it addresses your case.
- **Recommended:** read PR #2303 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member 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.

## Closed / duplicate issues

- #2228: bug: system error messages injected with dynamic role, can appear as role=user

## Prior & competing PRs

- **#2303: fix: remove synthetic error message injection, fix session resume after repeated failures**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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`
  - fix hunks:
  - `hermes_cli/plugins.py`

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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")
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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 |
    ```

    ```diff
    @@ -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
    ```
- **#2231: fix: always use role=assistant for system error messages**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#2303`
- **#2236: fix(agent): always use role=assistant for system error messages, never role=user**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#2253: fix: always use assistant role for system error messages**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#2303`
- **#2263: fix: bug: system error messages injected with dynamic role, can appear**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`

## Fix status

- Merged to main in PR #2303 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-21`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `agent`
