Skip to content

feat(gateway): optional speaker-name prefix on every stored message - #82552

Open
withzombies wants to merge 1 commit into
NousResearch:mainfrom
withzombies:feat/always-name-prefix
Open

feat(gateway): optional speaker-name prefix on every stored message#82552
withzombies wants to merge 1 commit into
NousResearch:mainfrom
withzombies:feat/always-name-prefix

Conversation

@withzombies

Copy link
Copy Markdown

What does this PR do?

Adds an opt-in knob — gateway.always_name_prefix in config.yaml / env var
HERMES_ALWAYS_NAME_PREFIX — that extends the [Name] sender prefix to
every stored inbound message, not just messages in shared multi-user
sessions.

Today the prefix in GatewayRunner._prepare_inbound_message_text fires only
when is_shared_multi_user_session(source) is True, where it disambiguates
participants. In a DM there is only one human, so the prefix is omitted — but
that makes DM and group transcripts structurally different, and a DM transcript
carries no durable record of who was speaking at all: any per-turn identity
context (channel banners, system-prompt notes) is ephemeral to the current
turn, while the stored message text is what survives in session history,
compaction, and session_search. Operators running one agent across several
1:1 channels (e.g. an assistant deployment fielding DMs from multiple people
on multiple platforms) have no way to keep attribution in history.

With the knob enabled every stored message carries the same
[<display name>] <text> shape already used in shared sessions, reusing the
exact same code path — including neutralize_untrusted_inline_text, so hostile
display names get the identical hardening. Default behavior is byte-identical
to today: env var unset ⇒ only shared multi-user sessions get the prefix.

Related Issue

No existing issue. Duplicate search performed (see checklist notes below);
closest existing work:

Fixes # (none — feature)

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • gateway/run.py
    • New module-level helper _always_prefix_speaker() (next to the other
      env readers, after _float_env): reads HERMES_ALWAYS_NAME_PREFIX,
      truthy values 1/true/yes/on (case-insensitive, whitespace
      tolerant). Read live so config reloads/tests take effect without
      re-import.
    • Sender-prefix guard in _prepare_inbound_message_text widened from
      if _is_shared_multi_user and source.user_name: to
      if (_is_shared_multi_user or _always_prefix_speaker()) and source.user_name:.
      The body — including the neutralize_untrusted_inline_text hardening and
      its security comment, and the Slack <@U...> author-mention append — is
      untouched.
    • Config→env bridge: gateway.always_name_prefix bridged to
      HERMES_ALWAYS_NAME_PREFIX, config-authoritative and normalized to
      "1"/"0" (same convention as the neighboring
      gateway.trust_recent_files key; unlike platform_connect_timeout,
      which is intentionally env-wins).
  • cli-config.yaml.example — commented gateway: block documenting
    always_name_prefix: false (placed after group_sessions_per_user, the
    related session-identity knob).
  • website/docs/reference/environment-variables.mdHERMES_ALWAYS_NAME_PREFIX
    row in the gateway env table (next to HERMES_GATEWAY_BUSY_ACK_ENABLED).
  • tests/gateway/test_always_name_prefix.py (new)
    • helper truthiness: default off; 1/true/yes/on (any case) on;
      empty/0/false/no/off/garbage off.
    • runtime, through the real _prepare_inbound_message_text (same harness
      as tests/gateway/test_shared_group_sender_prefix.py): a Telegram DM is
      stored bare with the env unset and as [Alice] hello there with it set;
      a hostile display name with embedded newlines is neutralized on the
      opt-in path; a shared Slack thread keeps its existing
      [Alice | Slack user <@U123>] prefix regardless of the knob.
  • tests/gateway/test_config_env_bridge_authority.py
    • HERMES_ALWAYS_NAME_PREFIX added to the subprocess env-key probe list.
    • Two bridge tests per the file's pattern: config true overrides a stale
      .env 0"1"; explicit config false overrides a stale .env
      1"0".

How to Test

  1. pytest tests/gateway/test_always_name_prefix.py tests/gateway/test_config_env_bridge_authority.py tests/gateway/test_shared_group_sender_prefix.py -q
    → 31 passed.
  2. Default unchanged: run the gateway with the knob unset, DM the bot, and
    inspect the stored session transcript (~/.hermes/sessions/) — the user
    message is stored bare, exactly as before.
  3. Opt in: add to ~/.hermes/config.yaml:
    gateway:
      always_name_prefix: true
    restart the gateway, DM the bot again — the stored user message is now
    [<your display name>] <text>, matching the shape group messages already
    have. (Or set HERMES_ALWAYS_NAME_PREFIX=1 in .env; config wins if both
    are set.)
  4. Group/thread behavior is unaffected in both states (shared sessions were
    already prefixed).

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (website/docs/reference/environment-variables.md)
  • I've updated cli-config.yaml.example (new gateway.always_name_prefix key)
  • I've updated CONTRIBUTING.md or AGENTS.md — N/A (no architecture/workflow change)
  • I've considered cross-platform impact — N/A (pure env/config read, no platform-specific code)
  • I've updated tool descriptions/schemas — N/A (no tool behavior change)

Screenshots / Logs

$ pytest tests/gateway/test_always_name_prefix.py tests/gateway/test_config_env_bridge_authority.py tests/gateway/test_shared_group_sender_prefix.py -q
...............................                                          [100%]
31 passed in 2.19s

$ uvx ruff@0.15.10 check gateway/ tests/
All checks passed!

🤖 Generated with Claude Code

The [Name] sender prefix is currently applied only in shared multi-user
sessions, where it disambiguates participants. DM messages are stored
bare, so a DM transcript carries no durable record of who was speaking —
any per-turn identity context is ephemeral, and DM vs group transcripts
end up structurally different.

Add an opt-in knob that extends the prefix to every stored inbound
message:

- gateway/run.py: _always_prefix_speaker() reads
  HERMES_ALWAYS_NAME_PREFIX (truthy: 1/true/yes/on, case-insensitive)
  and widens the sender-prefix guard in _prepare_inbound_message_text.
  The prefix reuses the existing neutralize_untrusted_inline_text path,
  so hostile display names are handled identically to shared sessions.
- Config bridge: gateway.always_name_prefix in config.yaml is bridged
  config-authoritatively to HERMES_ALWAYS_NAME_PREFIX (same convention
  as gateway.trust_recent_files, normalized to "1"/"0").
- cli-config.yaml.example + environment-variables.md document the knob.
- Tests: helper truthiness table, DM prefix on/off through the real
  _prepare_inbound_message_text, hostile-name neutralization, shared-
  session behavior unchanged, and config-bridge authority cases.

Default behavior is byte-identical: with the env var unset, only shared
multi-user sessions get the prefix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants