Repository navigation
Conversation
…Telegram `_build_gateway_agent_history` pulls `observed=True` rows out of replay history into a separate context-only block (wrapped in "[Observed ... context - context only, not requests]" / "[Current addressed message ...]"), but only when the turn's channel_prompt matched the literal `"observed Telegram group context"`. `gateway/platforms/yuanbao.py` (`GroupAtGuardMiddleware._observe_group_message`) persists observed group rows in the same `[nickname|user_id]\n<body>` shape as Telegram, and its per-turn prompt says "Lines in history prefixed with `[nickname|user_id]` are observed group context" — but that string does not contain the Telegram marker, so the splitter never fired. Yuanbao's observed rows replayed as ordinary user turns: a bystander's message reached the model with the same authority as the addressed user's, relying on the model honouring a prose prefix convention instead of the structural separation Telegram gets. Adds a platform-neutral marker (`"observed group context"`) that the matcher accepts alongside the Telegram one. Yuanbao's existing prompt already contains that substring, so it gains the fencing with no adapter change; Telegram behaviour is unchanged. The two context headers drop "Telegram" since they now front non-Telegram context too. Adapters that write observed rows without any marker keep the previous behaviour, so this is opt-in per prompt rather than a blanket change.
|
Correcting my own framing in the description above (edited): I initially tagged this as a security fix and described bystander text as arriving "with the addressed user's authority". That overstates it, and I'd rather retract it than have a reviewer refute it on first read. Yuanbao's So: a consistency and robustness gap between two adapters writing identical data, not a trust-boundary crossing. Per SECURITY.md §2/§3 that's why this is a public PR rather than a private advisory. The code change is unaffected. |
SummaryGeneralizes the observed-group-context fencing so What changed
Strengths
Findings
VerdictLooks good to merge (rename nit is non-blocking). Reviewed using Hermes-Agent |
|
Stale vs current NousResearch/main. The voice overlay now lives on Roxabi/roxabi-hermes@hermes-with-voice (product fork). Not landing this PR. |
What does this PR do?
_build_gateway_agent_historyliftsobserved=Truerows out of replay history into a separate context-only block, wrapped in[Observed ... context - context only, not requests]/[Current addressed message ...]. It only does so when the turn'schannel_promptcontains the literal"observed Telegram group context":Telegram is not the only adapter writing those rows.
gateway/platforms/yuanbao.py(GroupAtGuardMiddleware._observe_group_message) persists group messages in the same shape:To be precise about what is and isn't broken: Yuanbao is not unprotected. Its
_build_group_channel_prompttells the model exactly what those rows are:and its rows do carry that prefix. So Yuanbao has in-prompt fencing. What it does not get is the structural separation: the rows still replay as ordinary
userturns inconversation_history, and the[Observed ...]/[Current addressed message ...]wrapping never happens. Telegram gets both layers; Yuanbao gets one, and relies on the model honouring a prose convention.That asymmetry is the bug — two adapters writing identical data, one of them silently missing the mechanism designed for it, because the gate is keyed on a platform-specific string rather than on the contract.
Why this matters beyond Yuanbao
Five open PRs are adding observed-group-context to further platforms:
MessageDisposition.OBSERVE)inject_observed_group_contextopt-outEvery one of them inherits the same trap: write
observed=True, get no structural split unless the prompt happens to contain Telegram's string. Making the gate platform-neutral once here means those PRs don't each need to discover it.Interaction with #100961 (checked, no conflict): that PR adds
inject_observed_contextto_build_gateway_agent_historyand gates the append inside the existing branch:It never touches the marker constants or the matcher, so the two land in different hunks. Worth noting the ordering, though: because that whole branch is gated on
separate_observed_context, a platform whose prompt lacks the marker doesn't just miss the structural split — #100961's opt-out is unreachable for it too, since its observed rows never enter the branch at all. Fixing the gate is a prerequisite for that option to mean anything outside Telegram.The fix
Adds a platform-neutral marker (
"observed group context") accepted alongside the Telegram one. Yuanbao's existing prompt already contains that substring, so it gains the structural split with no adapter change; Telegram is untouched. An adapter that writes observed rows with no marker at all keeps today's behaviour, so this remains opt-in per prompt rather than a blanket change.Both context headers drop the word "Telegram", since they now front non-Telegram context too.
Related Issue
No existing issue. Searched open and closed PRs/issues for
observed group context,yuanbao observed,observed context marker,observed rows replay,observed context not fenced— the hits are the five sibling PRs above (all adding platforms, none fixing the gate) plus #48677 (observed context dropped before the API call, closed, different cause).Not filed through the private security channel: per SECURITY.md §2/§3 this is not a trust-boundary crossing — the in-prompt fencing is present, and what's missing is a robustness layer. Filing publicly per §3.2.
Type of Change
Changes Made
gateway/run.py— added_OBSERVED_CONTEXT_PROMPT_MARKER;_uses_telegram_observed_group_contextaccepts it alongside the Telegram marker;_OBSERVED_GROUP_CONTEXT_HEADERis no longer Telegram-specific. A comment states the contract: an adapter persistingobserved=Truerows must carry one of the markers or its rows replay as ordinary user turns.tests/gateway/test_telegram_group_gating.py—test_observed_rows_are_fenced_for_any_platform_prompt: real_build_gateway_agent_historycall with Yuanbao's actual prompt text; the observed row must land inobserved_contextand must not appear inagent_history.test_observed_rows_still_replay_without_any_marker: an unrelated prompt must not trigger the split (guards the generalized matcher against over-firing).How to Test
_observe_group_messagepersistsobserved=Truerows).userturns; no[Observed group context …]block is built.[Current addressed message …].Automated proof:
No regression in the surrounding areas:
test_telegram_group_gating.py+test_restart_resume_pending.py→66 passed;pytest tests/ -k "yuanbao or observed"→190 passed, 6 skipped.Checklist
Code
fix(scope):,feat(scope):, etc.)scripts/run_tests.shand it shows no new failures vsorigin/main(see "Full suite run" below);test_telegram_group_gating.py+test_restart_resume_pending.py→ 66 passed, and-k "yuanbao or observed"→ 190 passed, 6 skippedDocumentation & Housekeeping
docs/, docstrings) — or N/A (docstring + contract comment updated in place; no user-facing doc described the old gate)cli-config.yaml.exampleif I added/changed config keys — or N/A (no config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AFull suite run
Ran the canonical runner (
scripts/run_tests.sh -j 10, per-file isolation, hermetic env) on this branch and on a cleanorigin/mainworktree, same machine, same dedicated dev venv (Ubuntu Server 24.04, Python 3.12, 16 cores):origin/main(baseline)The two failure sets are identical —
diffof the per-file lists is empty. This branch introduces no new failures.The 106 pre-existing failures are environment-dependent on a workstation without the relevant credentials/binaries, and none are in a touched area:
test_daytona_environment(15),test_video_generation_tool_surface_matrix(38),test_fal_plugin(8),test_hindsight_provider(8),test_streaming(5),test_image_generation(3), bedrock/nous-portal/auxiliary-provider routing, browser profile/timeout, modal snapshots,test_web_tools_config,test_execution_flag_detection. Novoice,discord, ortelegram_groupfile appears in either list.