feat(gateway): config-driven per-user context injection (user_context_map) - #77117
feat(gateway): config-driven per-user context injection (user_context_map)#77117ctaylor86 wants to merge 2 commits into
Conversation
…_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>
1531e6b to
c3f3bf5
Compare
teknium1
left a comment
There was a problem hiding this comment.
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:3518resolves sender-specific context even for a shared session, andgateway/session.py:604-608renders it in the system prompt. Current main intentionally omits sender identity in shared sessions atgateway/session.py:556-568because it changes per turn. Since the prompt is incorporated into the agent-cache signature atgateway/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-410currently asserts that Alice's context is rendered in a shared session. The cache test at:269-296does not call_ephemeral_change_keyor the pin path, so it does not cover this regression.
Suggested changes
- Exclude
user_contextfrom 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.pythat asserts stable pinned prompt bytes and cache signature.
Automated hermes-sweeper review.
| 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, |
There was a problem hiding this comment.
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.
| # 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 |
There was a problem hiding this comment.
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.
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>
|
Thank you for the review. Both issues are addressed in commit 2b7cf6e: Shared-session cache contract: The Test coverage: The shared-session test is renamed and inverted to assert that context is resolved but NOT rendered. A new The slice 7/8 failures are in |
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:
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>:How it works
build_session_contextresolves the configured context once per session from the sender's platform-qualified identity (primaryuser_id, withuser_id_altfallback for Signal UUID / Feishu union_id).build_session_context_promptrenders it under**User Context:**after the sender identity line, before platform notes._pinned_session_context_promptmechanism, so it does not mutate mid-conversation._ephemeral_change_keyincludesuser_contextso a config change triggers exactly one re-render (a legitimate cache bust), not drift.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_keyhash includesuser_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, andprompt cache. The closest existing PRs are complementary, not duplicative:channel_context_map(per-CHAT context, not per-USER)per_user_profiles(workspace isolation, not context text injection)None provide config-driven per-user context text injection into the system prompt.
Type of Change
Changes Made
gateway/config.py: Addeduser_context_mapfield toGatewayConfig, parsing/validation infrom_dict, and config bridging inload_gateway_config(top-level + nestedgateway.*forms).gateway/session.py: Addeduser_contextfield toSessionContext, resolution inbuild_session_context, rendering inbuild_session_context_prompt.gateway/run.py: Addeduser_contextto_ephemeral_change_keyfor cache stability.hermes_cli/config.py: Addeduser_context_mapto_EXTRA_KNOWN_ROOT_KEYS.hermes_cli/config_defaults.py: Addeduser_context_maptoDEFAULT_CONFIG["gateway"].tests/gateway/test_user_context_map.py: 23 new tests.website/docs/user-guide/configuration.md: Documentation section.How to Test
user_context_mapentry for your platform user ID inconfig.yaml:**User Context:**section appears.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings)cli-config.yaml.exampleif I added/changed config keys — N/A (key documented in DEFAULT_CONFIG comments + configuration.md)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A