fix(gateway): preserve platform + gateway_session_key on /compress temp agent

Manual /compress built a temporary AIAgent without the originating
platform / stable gateway session key, so an external context engine
ingested the retained transcript tail as source=cli during /compress
and again as the real platform on resume (duplicate cli,telegram rows).
Pass platform=_platform_config_key(source.platform) + the in-scope
gateway_session_key, mirroring the normal gateway turn. Assigned into
runtime_kwargs (single-valued, authoritative) so they neither collide
into a duplicate-kwarg TypeError nor lose to a stale resolver value.

Fixes #50422.
This commit is contained in:
r266-tech 2026-06-22 09:30:49 +08:00 committed by kshitij
parent 00ec3b1884
commit 2a04137322
2 changed files with 96 additions and 0 deletions

View file

@ -3056,6 +3056,17 @@ class GatewaySlashCommandsMixin:
from agent.model_metadata import estimate_request_tokens_rough
session_key = self._session_key_for_source(source)
# Preserve the same platform + stable gateway session identity that a
# normal gateway turn passes (gateway/run.py main turn), so external
# context engines bind this temporary compression agent to the
# original platform conversation instead of falling back to an
# unbound/default "cli" host source — see #50422. _platform_config_key
# maps LOCAL->"cli" exactly like the live turn, avoiding a new
# "local" vs "cli" mismatch.
from gateway.run import _platform_config_key
platform_key = (
_platform_config_key(source.platform) if source.platform else None
)
model, runtime_kwargs = self._resolve_session_agent_runtime(
source=source,
session_key=session_key,
@ -3082,6 +3093,21 @@ class GatewaySlashCommandsMixin:
partial = False
head = msgs
# Bind the temporary compression agent to the originating source's
# platform + stable gateway session key. These are *authoritative*
# identity invariants (derived from `source`), so assign them into
# runtime_kwargs directly rather than via setdefault: a value already
# present there from the resolver would be a placeholder/stale
# identity and must not win. Assigning (vs passing a second explicit
# kwarg) also keeps each key single-valued, avoiding a "got multiple
# values for keyword argument" TypeError. platform is only set when
# known: for a source without platform metadata we leave it unset so
# AIAgent's default (platform=None -> source "cli") applies, exactly
# the prior behavior. _resolve_session_agent_runtime does not set
# either key today, so in practice this just adds them.
if platform_key is not None:
runtime_kwargs["platform"] = platform_key
runtime_kwargs["gateway_session_key"] = session_key
tmp_agent = AIAgent(
**runtime_kwargs,
model=model,

View file

@ -413,3 +413,73 @@ async def test_compress_command_in_place_write_failure_reports_error():
runner.session_store._save.assert_not_called()
agent_instance.shutdown_memory_provider.assert_called_once()
agent_instance.close.assert_called_once()
@pytest.mark.asyncio
async def test_compress_command_preserves_platform_and_gateway_session_key():
"""The temporary compression agent must carry the originating source's
platform and stable gateway session key, matching a normal gateway turn.
Without them ``_session_source_for_agent`` falls back to a default "cli"
host source, so an external context engine misattributes the retained
transcript tail and later duplicates it on resume (#50422)."""
history = _make_history()
runner = _make_runner(history)
agent_instance = MagicMock()
agent_instance.shutdown_memory_provider = MagicMock()
agent_instance.close = MagicMock()
agent_instance._cached_system_prompt = ""
agent_instance.tools = None
agent_instance.context_compressor.has_content_to_compress.return_value = True
agent_instance.session_id = "sess-1"
agent_instance._compress_context.return_value = (list(history), "")
with (
patch("gateway.run._resolve_runtime_agent_kwargs", return_value={"api_key": "test-key"}),
patch("gateway.run._resolve_gateway_model", return_value="test-model"),
patch("run_agent.AIAgent", return_value=agent_instance) as mock_agent,
patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100),
):
await runner._handle_compress_command(_make_event())
assert mock_agent.call_count == 1
_, kwargs = mock_agent.call_args
# Platform preserved as the live turn's config key (TELEGRAM -> "telegram"),
# not the unbound "cli"/"local" fallback.
assert kwargs.get("platform") == "telegram"
# Stable gateway session key preserved, identical to a normal gateway turn.
assert kwargs.get("gateway_session_key") == runner._session_key_for_source(_make_source())
assert kwargs["gateway_session_key"]
@pytest.mark.asyncio
async def test_compress_command_overrides_stale_resolver_identity():
"""If the resolver already supplies platform/gateway_session_key, the
construction must (a) not raise "got multiple values for keyword argument",
and (b) let the originating-source identity win a stale/placeholder
resolver value must not defeat the attribution fix."""
history = _make_history()
runner = _make_runner(history)
agent_instance = MagicMock()
agent_instance.shutdown_memory_provider = MagicMock()
agent_instance.close = MagicMock()
agent_instance._cached_system_prompt = ""
agent_instance.tools = None
agent_instance.context_compressor.has_content_to_compress.return_value = True
agent_instance.session_id = "sess-1"
agent_instance._compress_context.return_value = (list(history), "")
# Resolver injects a WRONG platform and a stale session key.
runtime = {"api_key": "test-key", "platform": "discord", "gateway_session_key": "stale-key"}
with (
patch("gateway.run._resolve_runtime_agent_kwargs", return_value=runtime),
patch("gateway.run._resolve_gateway_model", return_value="test-model"),
patch("run_agent.AIAgent", return_value=agent_instance) as mock_agent,
patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100),
):
await runner._handle_compress_command(_make_event()) # must not raise
assert mock_agent.call_count == 1
_, kwargs = mock_agent.call_args
# Source-derived identity overrides the stale resolver values, passed once.
assert kwargs["platform"] == "telegram"
assert kwargs["gateway_session_key"] == runner._session_key_for_source(_make_source())