From 43ddf50b04ccb14c199c538bfee108591f1a64c7 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:41:18 -0700 Subject: [PATCH] fix(gateway): expand @ references before the reply pointer so quoted text stays literal Follow-up to the truncation removal: with the full reply text in the prepared message, a `@file:`/`@url:` reference inside the *quoted* message reached the context-reference expander and read a local file on the replier's behalf (the old 500-char slice hid this for long quotes; short quotes were already exposed on main). Expansion now runs on the new message text only; the reply pointer is prepended afterwards. --- gateway/run_inbound.py | 9 +++++--- tests/gateway/test_reply_to_injection.py | 27 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/gateway/run_inbound.py b/gateway/run_inbound.py index b1e223a632..1208221fc5 100644 --- a/gateway/run_inbound.py +++ b/gateway/run_inbound.py @@ -1618,10 +1618,13 @@ class GatewayInboundMixin: message_text = await self._enrich_inbound_voice(event, source, message_text, audio_paths) message_text = self._prepend_inbound_media_file_notes(message_text, audio_file_paths, video_paths) message_text = self._prepend_inbound_document_notes(event, message_text) - message_text = self._prepend_inbound_reply_context(event, source, message_text) if "@" in message_text: - return await self._expand_inbound_context_references(source, session_key, message_text) - return message_text + message_text = await self._expand_inbound_context_references(source, session_key, message_text) + if message_text is None: + return None + # After expansion: the quoted reply is someone else's text and stays literal — an + # ``@file:`` inside it must never read a local file on the replier's behalf. + return self._prepend_inbound_reply_context(event, source, message_text) async def _prepare_profile_scoped_inbound_message_text( self, *, event: MessageEvent, source: SessionSource, history: List[Dict[str, Any]], diff --git a/tests/gateway/test_reply_to_injection.py b/tests/gateway/test_reply_to_injection.py index b536c8a24d..bc6f9fe7ab 100644 --- a/tests/gateway/test_reply_to_injection.py +++ b/tests/gateway/test_reply_to_injection.py @@ -86,6 +86,33 @@ async def test_telegram_long_reply_reaches_prompt_without_losing_later_items(): assert history == [{"role": "user", "content": "Previous request"}] +@pytest.mark.asyncio +async def test_quoted_reply_references_stay_literal_while_typed_ones_expand(tmp_path, monkeypatch): + """The replied-to author's ``@file:`` is quoted text, not the replier's request: no local read. + The same reference typed in the new message still expands (positive control).""" + import threading + + payload = tmp_path / "notes.txt" + payload.write_text("LOCAL-FILE-MARKER", encoding="utf-8") + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + runner = _make_runner() + runner._session_model_overrides, runner._last_resolved_model = {}, {} + runner._agent_cache, runner._agent_cache_lock = {}, threading.Lock() + runner._resolve_session_agent_runtime = lambda **kw: ("openai/gpt-4.1-mini", {"base_url": None, "api_key": ""}) + source = _source() + + quoted = ("x " * 300) + f"\nsee @file:{payload.name} for details" + quoted_ref = MessageEvent(text="what does this say?", source=source, reply_to_message_id="7", reply_to_text=quoted) + result = await runner._prepare_inbound_message_text(event=quoted_ref, source=source, history=[]) + assert quoted in result + assert "LOCAL-FILE-MARKER" not in result + + typed_ref = MessageEvent(text=f"read @file:{payload.name}", source=source, reply_to_message_id="7", reply_to_text="short") + result = await runner._prepare_inbound_message_text(event=typed_ref, source=source, history=[]) + assert result.startswith('[Replying to: "short"]') + assert "LOCAL-FILE-MARKER" in result + + @pytest.mark.asyncio async def test_reply_prefix_still_injected_when_text_in_history(): """Regression test: the pointer must survive even when the quoted text