Skip to content

feat(gateway): config-driven per-user context injection (user_context_map) - #77117

Open
ctaylor86 wants to merge 2 commits into
NousResearch:mainfrom
ctaylor86:feat/per-user-context-2026-08-03
Open

feat(gateway): config-driven per-user context injection (user_context_map)#77117
ctaylor86 wants to merge 2 commits into
NousResearch:mainfrom
ctaylor86:feat/per-user-context-2026-08-03

Conversation

@ctaylor86

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds an opt-in, config-driven per-user context field for gateway sessions so the agent can receive stable technical context guidance for the authenticated sender without editing SOUL.md. Preserves prompt caching and message alternation.

Problem

When a single gateway bot serves multiple users, the agent has no way to receive per-sender context guidance (communication preferences, technical background, domain conventions) without either:

  1. Editing SOUL.md (global, affects all users)
  2. Using per-user profiles (heavyweight: isolates entire HERMES_HOME)
  3. Building a custom adapter patch (not maintainable across updates)

There is no lightweight, config-driven way to attach stable context text to a specific sender identity. This PR fills that gap.

Solution

Add gateway.user_context_map — an inline config mapping keyed by canonical <platform>:<user_id>:

gateway:
  user_context_map:
    "telegram:123456789": "This user prefers bullet-point summaries."
    "discord:987654321": "Respond in British English."

How it works

  1. build_session_context resolves the configured context once per session from the sender's platform-qualified identity (primary user_id, with user_id_alt fallback for Signal UUID / Feishu union_id).
  2. build_session_context_prompt renders it under **User Context:** after the sender identity line, before platform notes.
  3. The value is pinned per session via the existing _pinned_session_context_prompt mechanism, so it does not mutate mid-conversation.
  4. _ephemeral_change_key includes user_context so a config change triggers exactly one re-render (a legitimate cache bust), not drift.
  5. Unlisted senders retain current behaviour (no extra context). Invalid entries fail closed to no extra context and produce a debug warning.

Cache safety

The resolved value is stable for the session lifetime. It flows through the same pinned-prompt path as every other session context field. The _ephemeral_change_key hash includes user_context, so a change in configured context for a sender triggers exactly one re-render and re-pin, matching the existing pattern for thread renames, redact_pii flips, etc.

Untrusted text handling

Values are rendered via _format_untrusted_prompt_value (JSON-quoted, newlines escaped, capped at 240 chars in the prompt metadata). This prevents a configured value from injecting fake markdown sections into the system prompt. Config validation also caps values at 4096 chars and keys at 256 chars, with a maximum of 4096 entries.

Related Issue

No existing issue. Searched open and closed PRs for per-user context, user context, sender context, user profile, runtime footer, sender identity, gateway session, and prompt cache. The closest existing PRs are complementary, not duplicative:

None provide config-driven per-user context text injection into the system prompt.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)

Changes Made

  • gateway/config.py: Added user_context_map field to GatewayConfig, parsing/validation in from_dict, and config bridging in load_gateway_config (top-level + nested gateway.* forms).
  • gateway/session.py: Added user_context field to SessionContext, resolution in build_session_context, rendering in build_session_context_prompt.
  • gateway/run.py: Added user_context to _ephemeral_change_key for cache stability.
  • hermes_cli/config.py: Added user_context_map to _EXTRA_KNOWN_ROOT_KEYS.
  • hermes_cli/config_defaults.py: Added user_context_map to DEFAULT_CONFIG["gateway"].
  • tests/gateway/test_user_context_map.py: 23 new tests.
  • website/docs/user-guide/configuration.md: Documentation section.

How to Test

  1. Add a user_context_map entry for your platform user ID in config.yaml:
    gateway:
      user_context_map:
        "telegram:<your_id>": "You are talking to a developer."
  2. Send a message to the bot and confirm the agent receives the configured context in its session prompt (visible in agent debug logs).
  3. Send a message from an unlisted user and confirm no **User Context:** section appears.
  4. Run:
    python3 -m pytest tests/gateway/test_user_context_map.py tests/gateway/test_session.py tests/gateway/test_config.py -q
    134 passed, 23 new tests pass. Pre-existing failures (Slack aiohttp, asyncio mark) are unchanged.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (key documented in DEFAULT_CONFIG comments + configuration.md)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — N/A (gateway config, platform-neutral)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

…_map)

Add gateway.user_context_map, an opt-in config section that injects
stable per-sender context text into the session system prompt without
editing SOUL.md. Keys use the canonical <platform>:<user_id> form.

Resolution:
- build_session_context resolves the configured context once per session
  from the sender's platform-qualified identity (primary user_id, with
  user_id_alt fallback for Signal UUID / Feishu union_id).
- build_session_context_prompt renders it under **User Context:** after
  the sender identity line, before platform notes.
- Unlisted senders retain current behaviour (no extra context).

Cache safety:
- The resolved value is pinned per session via the existing
  _pinned_session_context_prompt mechanism.
- _ephemeral_change_key includes user_context so a config change triggers
  exactly one re-render (legitimate cache bust), not drift.

Validation:
- Inline dict mapping only (no file-backed form).
- Keys stripped, max 256 chars. Values stripped, max 4096 chars (truncated).
- Max 4096 entries. Invalid entries silently dropped with debug log.
- Values rendered via _format_untrusted_prompt_value (JSON-quoted,
  newlines escaped) to prevent prompt injection.

Config forms:
- gateway.user_context_map (nested, written by hermes config set)
- top-level user_context_map (parity with profile_routes, multiplex_profiles)
- Added to DEFAULT_CONFIG and _EXTRA_KNOWN_ROOT_KEYS.

Tests: 23 new tests covering config parsing, validation, resolution
(known/unknown/alt-id/no-user-id), prompt rendering, cache stability,
and E2E injection. All pass. No new failures in existing gateway tests.

Signed-off-by: Carl Taylor <carl@carltaylor.com.au>
@ctaylor86
ctaylor86 force-pushed the feat/per-user-context-2026-08-03 branch from 1531e6b to c3f3bf5 Compare August 2, 2026 22:15

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused config, documentation, and validation work. The per-user mapping is not present on current main, but the shared-session path needs revision before it can preserve the gateway cache contract.

Problems

  • gateway/session.py:3518 resolves sender-specific context even for a shared session, and gateway/session.py:604-608 renders it in the system prompt. Current main intentionally omits sender identity in shared sessions at gateway/session.py:556-568 because it changes per turn. Since the prompt is incorporated into the agent-cache signature at gateway/run.py:4266-4274, alternating configured senders in one shared thread/group changes the prompt and rebuilds the agent.
  • tests/gateway/test_user_context_map.py:391-410 currently asserts that Alice's context is rendered in a shared session. The cache test at :269-296 does not call _ephemeral_change_key or the pin path, so it does not cover this regression.

Suggested changes

  • Exclude user_context from shared multi-user system prompts, or move any shared-session-specific design to a per-turn path that leaves the system prompt unchanged.
  • Add a two-sender shared-session regression to tests/gateway/test_prompt_tail_freeze.py that asserts stable pinned prompt bytes and cache signature.

Automated hermes-sweeper review.

Comment thread gateway/session.py
group_sessions_per_user=getattr(config, "group_sessions_per_user", True),
thread_sessions_per_user=getattr(config, "thread_sessions_per_user", False),
),
user_context=user_context,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value is resolved from the current sender even when shared_multi_user_session is true. In one shared thread/group, alternating senders will change the rendered system prompt and invalidate the cached agent; current main deliberately keeps sender-specific data out of that prompt. Please gate this to sender-isolated sessions or move shared-session context to a per-turn path.

Comment thread tests/gateway/test_user_context_map.py Outdated
# but the prompt uses session-level context. The context is still
# rendered since it's keyed on the sender, not the session type.
prompt = build_session_context_prompt(ctx)
assert "context for alice" in prompt No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asserts the unsafe shared-session behavior. Add an alternating-sender test through _pinned_session_context_prompt / the agent signature and assert that a shared session neither renders per-user context nor changes its cached system prompt.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 2, 2026
@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Aug 2, 2026
The triage sweeper correctly identified that rendering per-sender
user_context in the pinned system prompt for a shared multi-user session
breaks the prompt cache contract: alternating senders in one shared
thread/group would change the prompt bytes and rebuild the agent on
every turn switch (same reason sender identity is omitted at
session.py:556-568).

Fix: gate the User Context rendering on
not context.shared_multi_user_session, so per-sender context is
excluded from the cached system prompt in shared sessions. The value
is still resolved on the SessionContext object for potential per-turn
injection in a future follow-up.

Tests:
- Renamed test_shared_multi_user_session_no_user_context to
  test_shared_multi_user_session_excludes_user_context and inverted the
  assertion: context is resolved but NOT rendered in shared sessions.
- Added test_shared_session_cache_stability_across_senders: two
  different senders in the same shared session produce identical
  pinned system prompt bytes (no per-sender context leakage).

Signed-off-by: Carl Taylor <carl@carltaylor.com.au>
@ctaylor86

Copy link
Copy Markdown
Contributor Author

Thank you for the review. Both issues are addressed in commit 2b7cf6e:

Shared-session cache contract: The User Context rendering is now gated on not context.shared_multi_user_session, matching the existing pattern at session.py:556-568 where sender identity is omitted from shared sessions for the same cache-stability reason. Per-sender context is still resolved on the SessionContext object (for a potential per-turn injection path), but is not baked into the pinned system prompt in shared sessions.

Test coverage: The shared-session test is renamed and inverted to assert that context is resolved but NOT rendered. A new test_shared_session_cache_stability_across_senders test proves two different senders in the same shared session produce identical pinned system prompt bytes.

The slice 7/8 failures are in test_search_zero_match_and_multipath.py (zero-match search warnings) and are unrelated to this PR -- they pass locally and fail only in the CI container environment.

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/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants