修复综合模式历史会话泄漏内部指令。

将综合模式执行文本与用户落库文本彻底解耦,确保只持久化用户真实输入;同时在消息读取接口增加旧脏数据过滤,并补充回归测试,避免内部派单指令再次作为用户消息展示。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-29 00:02:30 +08:00
parent 048e8ec1c1
commit 8b8265eb46
7 changed files with 254 additions and 12 deletions

View file

@ -65,6 +65,7 @@ class AttemptRunnerInput:
wire_policy_role: str | None = None
prompt_build_context: dict[str, Any] | None = None
turn_uuid: str | None = None
persisted_user_text: str | None = None
@dataclass(frozen=True)
@ -133,6 +134,7 @@ def run_attempt(*, store: Any, data: AttemptRunnerInput) -> AttemptRunnerOutput:
workspace_dir=data.workspace_dir,
memory_context=data.memory_context,
persist_user_message=bool(data.persist_user_message),
persisted_user_text=data.persisted_user_text,
skill_binding_role=data.skill_binding_role,
wire_policy_role=data.wire_policy_role,
prompt_build_context=data.prompt_build_context,
@ -144,7 +146,7 @@ def run_attempt(*, store: Any, data: AttemptRunnerInput) -> AttemptRunnerOutput:
session_id=data.msg.session_id,
tenant_id=data.msg.tenant_id,
user_id=data.msg.user_id,
user_text=data.msg.text,
user_text=str(data.persisted_user_text if data.persisted_user_text is not None else data.msg.text or ""),
assistant_text=outcome.final_text,
turn_uuid=outcome.turn_uuid,
)

View file

@ -47,6 +47,7 @@ class AgentCoreRunInput:
oclaw_worker_id: str | None = None
skill_binding_role: str | None = None
wire_policy_role: str | None = None
persisted_user_text: str | None = None
@dataclass(frozen=True)
@ -218,6 +219,7 @@ def run_agent_core(*, store: Any, data: AgentCoreRunInput) -> AgentCoreRunOutput
wire_policy_role=data.wire_policy_role,
prompt_build_context=(dict(data.msg.metadata or {}) if isinstance(data.msg.metadata, dict) else None),
turn_uuid=user_turn_uuid,
persisted_user_text=data.persisted_user_text,
),
)
attempts.append(out.state)

View file

@ -920,6 +920,7 @@ def run_oclaw_direct_loop(
workspace_dir: str | None = None,
memory_context: OclawMemoryContext | None = None,
persist_user_message: bool = True,
persisted_user_text: str | None = None,
tool_signature_budget: int = 2,
skill_binding_role: str | None = None,
wire_policy_role: str | None = None,
@ -929,11 +930,12 @@ def run_oclaw_direct_loop(
"""A minimal oclaw-style loop: model -> tool_uses -> execute -> tool_results -> continue."""
_check_stop(should_stop)
turn_uuid = str(turn_uuid or "").strip() or str(uuid.uuid4())
persisted_text = str(user_text if persisted_user_text is None else persisted_user_text or "")
if persist_user_message:
store.add_message(
session_id=session_id,
role="user",
content=str(user_text or ""),
content=persisted_text,
attachments=attachments,
turn_uuid=turn_uuid,
event_type="user_text",

View file

@ -105,6 +105,26 @@ class OclawGateway:
def __init__(self, *, store: Any):
self.store = store
@staticmethod
def _looks_like_manager_instruction(reply: str, instruction: str) -> bool:
r = str(reply or "").strip()
ins = str(instruction or "").strip()
if not r or not ins:
return False
if r == ins:
return True
if len(ins) >= 16 and ins in r:
return True
# Heuristic: prefix overlap is usually enough for instruction leakage.
a = r[:120]
b = ins[:120]
common = 0
for x, y in zip(a, b):
if x != y:
break
common += 1
return common >= 24
@staticmethod
def _parse_json_object(text: str) -> dict[str, Any] | None:
t = str(text or "").strip()
@ -1178,6 +1198,7 @@ class OclawGateway:
store=self.store,
data=AgentCoreRunInput(
msg=exec_msg,
persisted_user_text=str(msg.text or ""),
lang=lang,
system_prompt=sys_prompt,
model=model,
@ -1202,15 +1223,12 @@ class OclawGateway:
executed_turn_uuid = str(getattr(core_out.outcome, "turn_uuid", "") or "")
specialist_reply = str(core_out.outcome.final_text or "")
if manager_memory_mode:
# manager_memory: write memory silently, but keep dialog output independent.
# User-facing reply comes from manager dispatch instruction_text.
reply = str(manager_instruction_text or "").strip()
if not reply:
reply = (
"已执行记忆写入。"
if not str(lang or "").startswith("en")
else "Memory write executed."
)
# manager_memory: never expose manager dispatch instruction_text to end users.
reply = (
"已执行记忆写入。"
if not str(lang or "").startswith("en")
else "Memory write executed."
)
elif interaction_mode == "comprehensive":
reply = self._manager_finalize_output(
msg=msg,
@ -1221,6 +1239,15 @@ class OclawGateway:
memory_enabled=memory_enabled,
on_token=on_token,
)
if self._looks_like_manager_instruction(reply, manager_instruction_text):
if not self._looks_like_manager_instruction(specialist_reply, manager_instruction_text):
reply = str(specialist_reply or "").strip()
else:
reply = (
"抱歉,我暂时无法给出可展示的结果,请稍后再试。"
if not str(lang or "").startswith("en")
else "Sorry, no user-safe result is available right now. Please try again later."
)
else:
reply = specialist_reply
except Exception as exc: