diff --git a/runtime/application/gateway/inbound_service.py b/runtime/application/gateway/inbound_service.py index 519b9c50..5175f61b 100644 --- a/runtime/application/gateway/inbound_service.py +++ b/runtime/application/gateway/inbound_service.py @@ -1453,15 +1453,21 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]: ) if str(lang or "").startswith("zh"): busy_text = ( - "本会话还有请求在处理,你这条已排队,完成后会继续回答。" - if bool(getattr(inbound, "is_group", False)) + "本群共享会话还有请求在处理,你这条已排队,完成后会继续回答。" + if ( + bool(getattr(inbound, "is_group", False)) + and str(group_policy.session_scope or "") == "chat" + ) else "还在处理上一条请求,这条会排队合并处理。" ) else: busy_text = ( - "Still working on an earlier request in this chat; " + "Still working on an earlier request in this shared group chat; " "your message is queued and will be answered next." - if bool(getattr(inbound, "is_group", False)) + if ( + bool(getattr(inbound, "is_group", False)) + and str(group_policy.session_scope or "") == "chat" + ) else "Still working on your previous request; " "I'll merge this follow-up next." ) diff --git a/runtime/gateway.py b/runtime/gateway.py index 079f14a8..c34c1bf8 100644 --- a/runtime/gateway.py +++ b/runtime/gateway.py @@ -486,11 +486,18 @@ class OclawGateway: @staticmethod def _group_focus_system_hint(msg: StandardMessage, lang: str) -> str: + """Only for shared group transcripts (legacy session_scope=chat / __group__).""" md = msg.metadata if isinstance(msg.metadata, dict) else {} if not bool(md.get("is_group")): return "" - from runtime.orchestration.group_ingest import build_group_focus_instruction + from runtime.orchestration.group_ingest import ( + build_group_focus_instruction, + normalize_group_session_scope, + ) + scope = normalize_group_session_scope(md.get("group_session_scope")) + if scope != "chat": + return "" return str(build_group_focus_instruction(lang=lang) or "").strip() @staticmethod diff --git a/runtime/orchestration/group_ingest.py b/runtime/orchestration/group_ingest.py index 5d22da38..f39b694e 100644 --- a/runtime/orchestration/group_ingest.py +++ b/runtime/orchestration/group_ingest.py @@ -590,11 +590,12 @@ def prepare_group_user_text_for_model( account_id=account_id, ) prefix_parts: list[str] = [] - # Always tag the speaker in groups (shared or per-user) so history/quotes cannot 串话. - _ = session_scope # retained for call-site compatibility / future policy forks - prefix_parts.append( - build_group_sender_context(metadata=metadata, external_user_id=external_user_id, lang=lang) - ) + # Shared group transcript only: tag speaker so multi-member history cannot 串话. + # Per-user group sessions (user_in_chat) already isolate by session — skip the prefix. + if normalize_group_session_scope(session_scope) == "chat": + prefix_parts.append( + build_group_sender_context(metadata=metadata, external_user_id=external_user_id, lang=lang) + ) quote = str(quoted_ctx or "").strip() if quote: prefix_parts.append(quote) @@ -605,14 +606,15 @@ def prepare_group_user_text_for_model( def build_group_focus_instruction(*, lang: str = "en") -> str: + """Isolation hint for *shared* group transcripts (session_scope=chat).""" if str(lang or "").strip().lower().startswith("zh"): return ( - "[群聊规则:只回答当前发言人(见 [发言: …])的问题;" + "[群聊规则(共享会话):只回答当前发言人(见 [发言: …])的问题;" "除非本条消息明确引用或承接前文,否则不要默认继承其他群成员的上下文;" "若排队合并了多条跟进,按条分别回应并 @ 对应发言人。]" ) return ( - "[Group chat rule: answer only the current sender (see [Sender: …]). " + "[Shared group-chat rule: answer only the current sender (see [Sender: …]). " "Do not assume context from other members unless this message explicitly quotes or references it. " "If several follow-ups were merged while busy, answer each item and @ the matching sender.]" ) diff --git a/tests/test_group_ingest.py b/tests/test_group_ingest.py index 92f05a26..00a3cbc9 100644 --- a/tests/test_group_ingest.py +++ b/tests/test_group_ingest.py @@ -400,8 +400,10 @@ def test_prepare_group_user_text_for_model_user_in_chat() -> None: mention_names=["吴华"], lang="zh", ) - assert out.startswith("[发言: Bob]") - assert "每三分钟提醒@吴华 喝水" in out + # Per-user group sessions already isolate by session — no sender prefix. + assert out == "每三分钟提醒@吴华 喝水" + assert "[发言:" not in out + assert "[Sender:" not in out def test_prepare_group_user_text_for_model_shared_chat_prefix() -> None: @@ -423,7 +425,7 @@ def test_build_group_focus_instruction() -> None: en = build_group_focus_instruction() zh = build_group_focus_instruction(lang="zh") assert "current sender" in en - assert "[Sender:" in en or "Sender" in en + assert "Shared group" in en or "shared" in en.lower() assert "群聊规则" in zh @@ -751,8 +753,9 @@ def test_inbound_group_mention_uses_per_user_session_and_sender_prefix( assert len(session_ids) == 2 assert session_ids[0] != session_ids[1] assert "[群成员:" not in captured["text"] - assert captured["text"].startswith("[Sender: Bob]") - assert captured["text"].endswith("again") + assert "[发言:" not in captured["text"] + assert "[Sender:" not in captured["text"] + assert captured["text"] == "again" assert "群聊规则" not in captured["text"] sid = store.get_or_create_channel_session_v2( diff --git a/tests/test_oclaw_gateway_trace.py b/tests/test_oclaw_gateway_trace.py index c2c553ec..db999046 100644 --- a/tests/test_oclaw_gateway_trace.py +++ b/tests/test_oclaw_gateway_trace.py @@ -956,10 +956,10 @@ def test_channel_file_delivery_hint_goes_to_system_not_user_message() -> None: assert not OclawGateway._is_channel_delivery_channel(msg_admin) -def test_group_focus_system_hint_only_for_groups() -> None: +def test_group_focus_system_hint_only_for_shared_group_scope() -> None: from runtime.types import StandardMessage - group_msg = StandardMessage( + shared = StandardMessage( session_id="s1", tenant_id="t1", user_id="u1", @@ -967,7 +967,17 @@ def test_group_focus_system_hint_only_for_groups() -> None: channel="whatsapp", text="@bot alarms", attachments=[], - metadata={"is_group": True}, + metadata={"is_group": True, "group_session_scope": "chat"}, + ) + per_user = StandardMessage( + session_id="s1", + tenant_id="t1", + user_id="u1", + role="member", + channel="whatsapp", + text="@bot alarms", + attachments=[], + metadata={"is_group": True, "group_session_scope": "user_in_chat"}, ) dm = StandardMessage( session_id="s1", @@ -979,6 +989,7 @@ def test_group_focus_system_hint_only_for_groups() -> None: attachments=[], metadata={"is_group": False}, ) - hint = OclawGateway._group_focus_system_hint(group_msg, "en") + hint = OclawGateway._group_focus_system_hint(shared, "en") assert "current sender" in hint + assert OclawGateway._group_focus_system_hint(per_user, "en") == "" assert OclawGateway._group_focus_system_hint(dm, "en") == ""