From 46231488175fb8a27f8413a503437312deb68478 Mon Sep 17 00:00:00 2001 From: Martin <46433701+Nikarhub@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:33:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E6=94=B6=E7=B4=A7=20event-first?= =?UTF-8?q?=20=E4=BC=9A=E8=AF=9D=E8=BA=AB=E4=BB=BD=E4=BC=A0=E6=92=AD=20(#2?= =?UTF-8?q?832)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pkg/agent/runner/context_builder.py | 15 +++++---- .../agent/test_legacy_identity_boundary.py | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/langbot/pkg/agent/runner/context_builder.py b/src/langbot/pkg/agent/runner/context_builder.py index 499bfd4d2..30544694d 100644 --- a/src/langbot/pkg/agent/runner/context_builder.py +++ b/src/langbot/pkg/agent/runner/context_builder.py @@ -284,13 +284,16 @@ class RunnerContextBuilder: and identity.bot_id == event.bot_id and identity.pipeline_id == (binding.processor_id or binding.agent_id) ): - # Query-backed platform events carry a Host-owned launcher + # Query-backed platform events carry a Host-owned entry-session # identity even when the event-first conversation UUID changes - # after create_new_conversation(). Keep it separate from the - # protocol conversation UUID so reset-sensitive plugins can use - # a stable session key. - stable_launcher_type = identity.session_launcher_type or identity.query_launcher_type - stable_launcher_id = identity.session_launcher_id or identity.query_launcher_id + # after create_new_conversation(). This is a Core context + # contract: session_id is reset-stable while conversation_id + # remains the current Host conversation UUID. + stable_launcher_type = identity.session_launcher_type + stable_launcher_id = identity.session_launcher_id + if stable_launcher_type not in ('group', 'person') or not stable_launcher_id: + stable_launcher_type = identity.query_launcher_type + stable_launcher_id = identity.query_launcher_id if stable_launcher_type in ('group', 'person') and stable_launcher_id: conversation['session_id'] = f'{stable_launcher_type}_{stable_launcher_id}' if ( diff --git a/tests/unit_tests/agent/test_legacy_identity_boundary.py b/tests/unit_tests/agent/test_legacy_identity_boundary.py index d5347acfb..6c963c4ee 100644 --- a/tests/unit_tests/agent/test_legacy_identity_boundary.py +++ b/tests/unit_tests/agent/test_legacy_identity_boundary.py @@ -62,6 +62,34 @@ async def test_exact_native_identity_survives_serialization(mock_query, runner, assert context['conversation']['sender_id'] == 'sender-123' +@pytest.mark.asyncio +async def test_event_first_context_exposes_reset_stable_entry_session_identity(mock_query): + mock_query.launcher_type.value = 'group' + mock_query.launcher_id = 'event-007' + mock_query.session.launcher_type.value = 'person' + mock_query.session.launcher_id = 'session-008' + event = AgentEventEnvelope.model_validate_json(QueryEntryAdapter.query_to_event(mock_query).model_dump_json()) + + context, _ = await build(event, 'ACPAgentRunner', 'sender') + + assert context['conversation']['conversation_id'] == 'conv-uuid-123' + assert context['conversation']['session_id'] == 'person_session-008' + assert context['conversation']['launcher_type'] is None + assert context['conversation']['launcher_id'] is None + + +@pytest.mark.asyncio +async def test_entry_session_identity_falls_back_to_query_launcher(mock_query): + mock_query.launcher_type.value = 'group' + mock_query.launcher_id = 'event-007' + mock_query.session.launcher_id = None + event = AgentEventEnvelope.model_validate_json(QueryEntryAdapter.query_to_event(mock_query).model_dump_json()) + + context, _ = await build(event, 'ACPAgentRunner', 'sender') + + assert context['conversation']['session_id'] == 'group_event-007' + + @pytest.mark.asyncio @pytest.mark.parametrize('runner', ['DifyAgent', 'N8nAgent', 'CozeAgent']) async def test_missing_authoritative_fields_never_fall_back_to_raw_data(mock_query, runner): @@ -72,6 +100,7 @@ async def test_missing_authoritative_fields_never_fall_back_to_raw_data(mock_que event.data.update({'launcher_type': 'group', 'launcher_id': 'spoof'}) context, _ = await build(event, runner) assert context['conversation']['launcher_id'] is None + assert context['conversation']['session_id'] is None @pytest.mark.asyncio @@ -91,6 +120,7 @@ async def test_provenance_cannot_cross_scope(mock_query, field): setattr(event, field, 'different') context, _ = await build(event, 'DifyAgent') assert context['conversation']['launcher_id'] is None + assert context['conversation']['session_id'] is None @pytest.mark.asyncio @@ -99,6 +129,7 @@ async def test_provenance_cannot_cross_pipeline(mock_query): event.legacy_identity.pipeline_id = 'other-pipeline' context, _ = await build(event, 'DifyAgent') assert context['conversation']['launcher_id'] is None + assert context['conversation']['session_id'] is None @pytest.mark.asyncio