Merge remote-tracking branch 'origin/master' into fix/memory-fact-integrity

This commit is contained in:
huanghuoguoguo
2026-11-02 21:13:10 +08:00
2 changed files with 40 additions and 6 deletions
@@ -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 (
@@ -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