fix(agent): improve cross-channel session persist robustness

This commit is contained in:
whs
2026-04-16 15:04:00 +08:00
committed by chengyongru
parent 1747ed7885
commit 5a401464a5
3 changed files with 178 additions and 9 deletions
+153 -3
View File
@@ -409,7 +409,9 @@ def test_cross_channel_same_session_not_duplicated(tmp_path: Path) -> None:
assert all(not m.get("_cross_channel") for m in session.messages)
def test_cross_channel_target_session_not_exist(tmp_path: Path) -> None:
def test_cross_channel_target_session_not_exist_creates_session(tmp_path: Path) -> None:
"""When the target session does not exist yet, get_or_create will create it
and the cross-channel message should still be persisted."""
loop = _make_full_loop(tmp_path)
source_session = loop.sessions.get_or_create("websocket:ws-xyz")
@@ -418,8 +420,126 @@ def test_cross_channel_target_session_not_exist(tmp_path: Path) -> None:
)
loop._save_turn(source_session, msgs, skip=1)
# Target session was never created, so no error should occur
assert "feishu:ou_nonexistent" not in loop.sessions._cache
# Target session is now auto-created with the cross-channel message
target = loop.sessions.get_or_create("feishu:ou_nonexistent")
cross_msgs = [m for m in target.messages if m.get("_cross_channel")]
assert len(cross_msgs) == 1
assert cross_msgs[0]["content"] == "Report: audit complete"
def test_cross_channel_persists_media_attachments(tmp_path: Path) -> None:
"""When the message tool call includes media, the cross-channel entry
should preserve the media paths so the target session has full context."""
loop = _make_full_loop(tmp_path)
source_key = "websocket:ws-media"
target_key = "telegram:tg_user1"
target_session = loop.sessions.get_or_create(target_key)
target_session.add_message("user", "waiting for report")
loop.sessions.save(target_session)
source_session = loop.sessions.get_or_create(source_key)
msgs = [
{"role": "user", "content": "send chart to telegram"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_m1",
"type": "function",
"function": {
"name": "message",
"arguments": json.dumps({
"content": "Here is the chart",
"channel": "telegram",
"chat_id": "tg_user1",
"media": ["/tmp/chart.png", "/tmp/data.csv"],
}),
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_m1",
"name": "message",
"content": "Message sent to telegram:tg_user1",
},
]
loop._save_turn(source_session, msgs, skip=1)
loop.sessions.invalidate(target_key)
target = loop.sessions.get_or_create(target_key)
cross_msgs = [m for m in target.messages if m.get("_cross_channel")]
assert len(cross_msgs) == 1
assert cross_msgs[0]["content"] == "Here is the chart"
assert cross_msgs[0]["_media"] == ["/tmp/chart.png", "/tmp/data.csv"]
def test_cross_channel_records_source_session(tmp_path: Path) -> None:
"""Cross-channel entries should include _source_session for traceability."""
loop = _make_full_loop(tmp_path)
source_key = "cron:heartbeat"
target_key = "feishu:ou_trace"
target_session = loop.sessions.get_or_create(target_key)
target_session.add_message("user", "hi")
loop.sessions.save(target_session)
source_session = loop.sessions.get_or_create(source_key)
msgs = _cross_channel_messages(
source_channel="cron", source_chat_id="heartbeat",
target_channel="feishu", target_chat_id="ou_trace",
)
loop._save_turn(source_session, msgs, skip=1)
loop.sessions.invalidate(target_key)
target = loop.sessions.get_or_create(target_key)
cross_msgs = [m for m in target.messages if m.get("_cross_channel")]
assert len(cross_msgs) == 1
assert cross_msgs[0]["_source_session"] == "cron:heartbeat"
def test_cross_channel_media_only_no_content(tmp_path: Path) -> None:
"""A message with media but empty content should still be persisted."""
loop = _make_full_loop(tmp_path)
target_key = "discord:ch_img"
target_session = loop.sessions.get_or_create(target_key)
loop.sessions.save(target_session)
source_session = loop.sessions.get_or_create("websocket:ws-img")
msgs = [
{"role": "user", "content": "send image"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_img",
"type": "function",
"function": {
"name": "message",
"arguments": json.dumps({
"content": "",
"channel": "discord",
"chat_id": "ch_img",
"media": ["/tmp/photo.jpg"],
}),
},
}
],
},
{"role": "tool", "tool_call_id": "call_img", "name": "message", "content": "ok"},
]
loop._save_turn(source_session, msgs, skip=1)
loop.sessions.invalidate(target_key)
target = loop.sessions.get_or_create(target_key)
cross_msgs = [m for m in target.messages if m.get("_cross_channel")]
assert len(cross_msgs) == 1
assert cross_msgs[0]["_media"] == ["/tmp/photo.jpg"]
def test_cross_channel_non_message_tools_ignored(tmp_path: Path) -> None:
@@ -453,3 +573,33 @@ def test_cross_channel_non_message_tools_ignored(tmp_path: Path) -> None:
target = loop.sessions.get_or_create("feishu:ou_tgt")
cross_msgs = [m for m in target.messages if m.get("_cross_channel")]
assert len(cross_msgs) == 0
def test_cross_channel_get_history_annotates_provenance(tmp_path: Path) -> None:
"""get_history() should prefix cross-channel messages with source info
so the LLM knows where the message came from."""
loop = _make_full_loop(tmp_path)
target_key = "feishu:ou_hist"
target_session = loop.sessions.get_or_create(target_key)
target_session.add_message("user", "hello")
# Simulate a cross-channel entry as _persist_cross_channel_calls would create
target_session.messages.append({
"role": "assistant",
"content": "Daily report ready",
"_cross_channel": True,
"_source_session": "cron:daily-report",
})
loop.sessions.save(target_session)
loop.sessions.invalidate(target_key)
target = loop.sessions.get_or_create(target_key)
history = target.get_history()
# Find the annotated message
annotated = [m for m in history if "cron:daily-report" in m.get("content", "")]
assert len(annotated) == 1
assert annotated[0]["content"] == "[Sent from cron:daily-report] Daily report ready"
# Internal metadata keys should NOT leak into the history output
assert "_cross_channel" not in annotated[0]
assert "_source_session" not in annotated[0]