From 2ed89fdc029644be35f254982f689ed0d78de22b Mon Sep 17 00:00:00 2001 From: "lusendong.6789" Date: Wed, 11 Nov 2026 12:09:20 +0800 Subject: [PATCH] feat(reward-memory): skip duplicate turn reflections Signed-off-by: lusendong.6789 Co-authored-by: TRAE CLI --- loopx/capabilities/reward_memory/README.md | 13 +++++++++++-- loopx/capabilities/reward_memory/README.zh-CN.md | 8 +++++++- loopx/capabilities/reward_memory/turn_hooks.py | 4 ++++ tests/control_plane/test_quota_memory_context.py | 8 ++++++++ .../test_reward_memory_host_isolation.py | 10 ++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/loopx/capabilities/reward_memory/README.md b/loopx/capabilities/reward_memory/README.md index 6ca4517fef..9ad3265354 100644 --- a/loopx/capabilities/reward_memory/README.md +++ b/loopx/capabilities/reward_memory/README.md @@ -287,8 +287,17 @@ engineering source kind, and opaque evidence refs; an ordinary Turn summary is not evidence. It also carries `procedural_experience_contract_v0` with applicability, observed outcome, attribution, complete future behavior, limitations, and the same evidence refs. Legacy v0 reflections remain -audit-only and cannot become durable memory. Ambiguous provider commits and -unverified readbacks remain in a +audit-only and cannot become durable memory. When the same Turn delivers recall +guidance and enables ingest, reflection compares new independent evidence with +comparable procedural experience guidance. Without a comparable experience, the +ordinary eligibility rule remains in force. Mere confirmation leaves the +reflection empty; a candidate is +produced only for distinct reusable applicability or a material change to its +trigger, action, validation, stop condition, or limitations. Distinct +applicability remains separate rather than being blended. Reflection never +merges, updates, retires, or supersedes a provider record; configured lifecycle +owners retain those decisions. Ambiguous +provider commits and unverified readbacks remain in a mode-0600 Goal+Agent+event sidecar. The next executing Turn retries the same deterministic event before recall, so the provider can deduplicate it and LoopX can require exact readback. Explicit disable suppresses reconciliation and all diff --git a/loopx/capabilities/reward_memory/README.zh-CN.md b/loopx/capabilities/reward_memory/README.zh-CN.md index 74d04970c1..b9bb501b59 100644 --- a/loopx/capabilities/reward_memory/README.zh-CN.md +++ b/loopx/capabilities/reward_memory/README.zh-CN.md @@ -187,7 +187,13 @@ quota settlement 均完成后,才接受 outcome ingest。Reflection 必须使 research/simulation/real/engineering 来源类型和 opaque evidence refs;还必须携带 `procedural_experience_contract_v0`,分别写出适用情境、已观察结果、归因、未来行为(触发、动作、 验证、停止条件)、失效边界,并绑定相同 evidence refs。只有事实复述、没有未来行为改变的普通 -Turn summary 不算经验。旧的 v0 reflection 只保留审计兼容,不会写成可持久召回的记忆。 +Turn summary 不算经验。旧的 v0 reflection 只保留审计兼容,不会写成可持久召回的记忆。同一 +Turn 同时交付 recall guidance 并启用 ingest 时,reflection 会把新的独立证据与其中可比较的 +procedural experience 对照;没有可比较经验时,仍使用普通资格规则。若只是确认相同的未来行为 +与失效边界,就保持为空。只有触发、动作、验证、停止条件或 +失效边界发生实质变化,或者形成可复用的独立适用情境时才生成候选;适用情境不同的经验保持 +分开,不混成更宽泛的规则。 +Reflection 不合并、更新、退役或取代 Provider 记录,这些决定仍属于已配置的生命周期 owner。 Provider commit 含糊或精确读回失败时,会保留一份权限为 0600、按 Goal+Agent+事件隔离的 sidecar。下一个执行 Turn 会在 recall 前以同一 deterministic event 重试,使 Provider 可以去重, LoopX 再要求精确读回。显式关闭会停止 reconciliation,并保持零 Provider 调用。 diff --git a/loopx/capabilities/reward_memory/turn_hooks.py b/loopx/capabilities/reward_memory/turn_hooks.py index 836e3ae870..57cd43394d 100644 --- a/loopx/capabilities/reward_memory/turn_hooks.py +++ b/loopx/capabilities/reward_memory/turn_hooks.py @@ -30,6 +30,10 @@ def _reward_memory_prompt_instructions(*, has_guidance: bool, ingest_enabled: bo instructions.append( "Set reward_memory_reflection_json to an empty string unless independent task evidence established a reusable experience. For eligible evidence, return one compact JSON object using schema_version=turn_reward_memory_reflection_v1, status=eligible, a configured surface_id, outcome_kind in research|simulation|real|engineering, content_summary, reasoning_summary, confidence in low|medium|high, and 1-5 opaque evidence_refs. Also include experience using schema_version=procedural_experience_contract_v0 with non-empty applicability and limitations lists, observed_outcome, attribution, the same evidence_refs, and future_behavior containing trigger, action, validation, and stop_condition. A fact recap without a future behavior change and non-generalization boundary is not eligible memory. Legacy v0 reflections are audit-only and cannot become durable memory. Never use your own summary as evidence. Settlement may ingest it only when the caller-declared Todo validator attests the exact reflection digest and evidence; ordinary validator success remains awaiting and makes no provider write." ) + if has_guidance and ingest_enabled: + instructions.append( + "Before proposing a reflection, compare current independent task evidence with any comparable procedural_experience in the delivered reward_memory_recall guidance. If no comparable procedural experience was delivered, apply the ordinary eligibility rule above. Leave reward_memory_reflection_json empty when the evidence only confirms the same future behavior and limitations; emit a candidate only when it establishes distinct reusable applicability or materially changes the trigger, action, validation, stop condition, or limitations. Keep distinct applicability conditions separate rather than blending them. Do not claim to merge, update, retire, or supersede provider records; those lifecycle decisions remain with their configured owners." + ) return instructions diff --git a/tests/control_plane/test_quota_memory_context.py b/tests/control_plane/test_quota_memory_context.py index f821104087..1946d39e60 100644 --- a/tests/control_plane/test_quota_memory_context.py +++ b/tests/control_plane/test_quota_memory_context.py @@ -86,6 +86,10 @@ def test_real_app_guard_delivers_scoped_memory_once( assert project_turn_context(first)["reward_memory_recall"]["context"]["guidance"] == guidance assert contribution["result_fields"]["reward_memory_reflection_json"]["maxLength"] == 2400 assert "private, non-authoritative" in contribution["instructions"][0] + assert any( + "compare current independent task evidence" in item + for item in contribution["instructions"] + ) repeated = run_cli(args) assert repeated["ok"] and memory.calls == 1 assert not repeated.get("reward_memory_recall", {}).get("external_writes_performed") @@ -206,6 +210,10 @@ def test_quota_recall_size_limit_preserves_independently_enabled_ingest(size: in delivered = result["context_fields"]["reward_memory_recall"] assert (delivered["context"] is not None) is (size == 40_000) assert result["result_fields"]["reward_memory_reflection_json"]["maxLength"] == 2400 + assert any( + "compare current independent task evidence" in item + for item in result["instructions"] + ) is (size == 40_000) assert len(json.dumps(result, separators=(",", ":")).encode()) <= 65536 diff --git a/tests/control_plane/test_reward_memory_host_isolation.py b/tests/control_plane/test_reward_memory_host_isolation.py index 7d8142a36c..e4c7c0c4d6 100644 --- a/tests/control_plane/test_reward_memory_host_isolation.py +++ b/tests/control_plane/test_reward_memory_host_isolation.py @@ -88,6 +88,16 @@ def test_recall_and_reflection_are_independently_opted_in(recall: bool, ingest: prompt = _prompt(request) assert ("When reward_memory_recall contains guidance" in prompt) is recall assert ("Set reward_memory_reflection_json" in prompt) is ingest + assert ("compare current independent task evidence" in prompt) is (recall and ingest) + for consolidation_rule in ( + "any comparable procedural_experience", + "If no comparable procedural experience was delivered", + "only confirms the same future behavior and limitations", + "establishes distinct reusable applicability or materially changes", + "Keep distinct applicability conditions separate", + "Do not claim to merge, update, retire, or supersede provider records", + ): + assert (consolidation_rule in prompt) is (recall and ingest) assert ("reward_memory_reflection_json" in codex_cli_result_schema(request)["properties"]) is ingest assert bool(request["reward_memory_recall"].get("context")) is recall if ingest: