From 84f7d79d3740450a33aba0a2224a28e7e997a788 Mon Sep 17 00:00:00 2001 From: hyk <4408344+hhyykk@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:33:25 +0800 Subject: [PATCH 1/2] fix(reward-memory): project effective Agent automation admission Signed-off-by: hyk <4408344+hhyykk@users.noreply.github.com> --- .../reward_memory/configuration.py | 47 +++++++++++++++++-- loopx/cli_commands/support_control.py | 18 +++---- .../project_registry_io_manifest_v1.json | 4 +- loopx/upgrade.py | 15 +++--- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/loopx/capabilities/reward_memory/configuration.py b/loopx/capabilities/reward_memory/configuration.py index f36e9e0bdd..1f7664c549 100644 --- a/loopx/capabilities/reward_memory/configuration.py +++ b/loopx/capabilities/reward_memory/configuration.py @@ -56,6 +56,8 @@ def reward_memory_goal_configuration_summary( and receipt.get("exact_readback_verified") is True ) effective_verified_agents: list[str] = [] + configured_automatic_ingest = False + configured_automatic_recall = False for agent_id in recorded_verified_agents: try: status, _ = resolve_goal_reward_memory_experiment( @@ -65,6 +67,14 @@ def reward_memory_goal_configuration_summary( continue if status.get("available") is True: effective_verified_agents.append(agent_id) + configured_automatic_ingest = ( + configured_automatic_ingest + or status.get("automatic_ingest") is True + ) + configured_automatic_recall = ( + configured_automatic_recall + or status.get("automatic_recall") is True + ) effective_available = bool(effective_verified_agents) return { "enabled": policy["enabled"], @@ -75,10 +85,10 @@ def reward_memory_goal_configuration_summary( "experimental": policy["experimental"], "config_pointer_registered": bool(policy["config_path"]), "binding_revision": binding_revision, - "automatic_ingest": effective_available - and policy["automation"].get("automatic_ingest") is True, - "automatic_recall": effective_available - and policy["automation"].get("automatic_recall") is True, + "automatic_ingest": policy["automation"].get("automatic_ingest") is True + and configured_automatic_ingest, + "automatic_recall": policy["automation"].get("automatic_recall") is True + and configured_automatic_recall, "automation_intent": dict(policy["automation_intent"]), "host_coverage": reward_memory_host_coverage(), "enabled_agents": list(policy["enabled_agents"]), @@ -86,6 +96,35 @@ def reward_memory_goal_configuration_summary( } +def reward_memory_automatic_ingest_for_agent( + goal: Mapping[str, Any], agent_id: str | None +) -> bool: + """Return whether one Agent may receive automatic-ingest prompt guidance. + + Desired policy is a cheap off switch. Active admission reuses the runtime + resolver so the prompt requires this exact Agent's current config binding + and verified enablement receipt. The resolver is read-only and never calls + the configured provider. + """ + + policy = reward_memory_goal_policy(goal) + if ( + not policy["enabled"] + or policy["automation"].get("automatic_ingest") is not True + or not isinstance(agent_id, str) + or not agent_id.strip() + ): + return False + try: + status, _ = resolve_goal_reward_memory_experiment( + goal=goal, agent_id=agent_id + ) + except ValueError: + # Legacy alias-only registrations and malformed identities fail closed. + return False + return status.get("available") is True and status.get("automatic_ingest") is True + + def plan_reward_memory_goal_configuration( *, goal: Mapping[str, Any], diff --git a/loopx/cli_commands/support_control.py b/loopx/cli_commands/support_control.py index 68efa11d48..3df62781d9 100644 --- a/loopx/cli_commands/support_control.py +++ b/loopx/cli_commands/support_control.py @@ -15,7 +15,6 @@ DEFAULT_CHAT_PORT, serve_chat, ) -from ..control_plane.reward_memory import reward_memory_goal_policy from ..control_plane.scheduler.execution_context import SchedulerRuntimeProfile from ..dashboard_launcher import launch_dashboard, replace_existing_loopx_chat from ..execution_profile import execution_profile_turn_granularity @@ -313,13 +312,6 @@ def handle_support_control_command( if isinstance(registry_goal, dict) else None ) - reward_memory_policy = reward_memory_goal_policy( - registry_goal if isinstance(registry_goal, dict) else {} - ) - reward_memory_enabled = bool( - reward_memory_policy["enabled"] - and reward_memory_policy["automation"].get("automatic_ingest") is True - ) agent_profile = None if args.agent_id: effective_agent_id = require_registered_agent_id( @@ -331,6 +323,16 @@ def handle_support_control_command( agent_profile = agent_profile_from_registry( agent_registry_path, args.goal_id, effective_agent_id ) + reward_memory_enabled = False + if effective_agent_id is not None: + from ..capabilities.reward_memory.configuration import ( + reward_memory_automatic_ingest_for_agent, + ) + + reward_memory_enabled = reward_memory_automatic_ingest_for_agent( + registry_goal if isinstance(registry_goal, dict) else {}, + effective_agent_id, + ) explicit_scheduler_fields = ( args.host_surface, args.scheduler_owner, diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index 8e08a720d6..4dcc12fe68 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -2287,7 +2287,7 @@ }, { "site": "loopx/upgrade.py::.build_upgrade_plan::codec_read:load_registry#1", - "line": 741, + "line": 744, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -2295,7 +2295,7 @@ }, { "site": "loopx/upgrade.py::.build_upgrade_plan::codec_read:load_registry#2", - "line": 747, + "line": 750, "column": 24, "kind": "codec_read", "api": "load_registry", diff --git a/loopx/upgrade.py b/loopx/upgrade.py index d6f2602514..3a2d2cc99c 100644 --- a/loopx/upgrade.py +++ b/loopx/upgrade.py @@ -14,7 +14,6 @@ ) from .execution_profile import execution_profile_turn_granularity from .heartbeat_prompt import build_heartbeat_prompt -from .control_plane.reward_memory import reward_memory_goal_policy from .history import load_registry from .paths import global_registry_path, resolve_runtime_root, select_default_runtime_root from .registry import registry_goals, resolve_state_file @@ -85,11 +84,15 @@ def goal_heartbeat_prompt( repo = Path(str(goal.get("repo") or ".")).expanduser() state_file = resolve_state_file(repo, goal.get("state_file")) registered_agents = registered_agent_ids_for_goal(goal) - reward_memory_policy = reward_memory_goal_policy(goal) - reward_memory_enabled = bool( - reward_memory_policy["enabled"] - and reward_memory_policy["automation"].get("automatic_ingest") is True - ) + reward_memory_enabled = False + if agent_id is not None: + from .capabilities.reward_memory.configuration import ( + reward_memory_automatic_ingest_for_agent, + ) + + reward_memory_enabled = reward_memory_automatic_ingest_for_agent( + goal, agent_id + ) return build_heartbeat_prompt( goal_id=goal_id, active_state=None, From 7354b4c2f34daff2f3a8bb49b4772d5b5533fc38 Mon Sep 17 00:00:00 2001 From: hyk <4408344+hhyykk@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:33:25 +0800 Subject: [PATCH 2/2] test(reward-memory): cover prompt and effective settings admission Signed-off-by: hyk <4408344+hhyykk@users.noreply.github.com> --- .../test_reward_memory_experiment.py | 308 +++++++++++++++++- 1 file changed, 307 insertions(+), 1 deletion(-) diff --git a/tests/capabilities/test_reward_memory_experiment.py b/tests/capabilities/test_reward_memory_experiment.py index da25fec130..a9f69472ae 100644 --- a/tests/capabilities/test_reward_memory_experiment.py +++ b/tests/capabilities/test_reward_memory_experiment.py @@ -1564,7 +1564,13 @@ def test_catalog_distinguishes_cached_receipt_from_live_config(tmp_path: Path) - ) registry_path, _, _ = _experiment(tmp_path) - goal = json.loads(registry_path.read_text())["goals"][0] + goal, _ = _configure_prompt_ingest_binding( + registry_path, + config_automatic_ingest=True, + policy_automatic_ingest=True, + config_automatic_recall=True, + policy_automatic_recall=True, + ) goal["control_plane"]["reward_memory"]["automation"] = { "automatic_recall": True, "automatic_ingest": True, @@ -1613,6 +1619,61 @@ def test_catalog_distinguishes_cached_receipt_from_live_config(tmp_path: Path) - ) +def test_effective_automation_requires_live_config_flags_in_catalog( + tmp_path: Path, +) -> None: + from loopx.capabilities.reward_memory.configuration import ( + reward_memory_goal_configuration_summary, + ) + from loopx.configuration_catalog import build_goal_configuration_catalog + + registry_path, _, _ = _experiment(tmp_path) + goal = json.loads(registry_path.read_text(encoding="utf-8"))["goals"][0] + policy = goal["control_plane"]["reward_memory"] + live_config = json.loads( + (Path(goal["repo"]) / policy["config_path"]).read_text(encoding="utf-8") + ) + assert live_config["automation"]["automatic_recall"] is False + assert live_config["automation"]["automatic_ingest"] is False + policy["automation"] = {"automatic_recall": True, "automatic_ingest": True} + + summary = reward_memory_goal_configuration_summary(goal) + assert summary["effective_available"] is True + assert summary["desired_automation"] == { + "automatic_recall": True, + "automatic_ingest": True, + } + assert summary["automatic_recall"] is False + assert summary["automatic_ingest"] is False + + catalog = build_goal_configuration_catalog( + goal_id=goal["id"], + settings={}, + feature_summary={"reward_memory": summary}, + default_multi_subagent_max_children=4, + explore_harness_profiles=[], + ) + current = next( + feature["current"] + for feature in catalog["features"] + if feature["feature_id"] == "reward_memory" + ) + assert current["effective_available"] is True + assert current["desired_automation"]["automatic_recall"] is True + assert current["automatic_recall"] is False + assert current["automatic_ingest"] is False + + policy["automation"] = {"automatic_recall": False, "automatic_ingest": False} + feature_off = reward_memory_goal_configuration_summary(goal) + assert feature_off["effective_available"] is True + assert feature_off["desired_automation"] == { + "automatic_recall": False, + "automatic_ingest": False, + } + assert feature_off["automatic_recall"] is False + assert feature_off["automatic_ingest"] is False + + @pytest.mark.parametrize( ("field", "invalid"), [ @@ -1645,3 +1706,248 @@ def test_catalog_availability_uses_runtime_receipt_checks( assert summary["effective_available"] is False assert summary["enablement_verified_agents"] == [] assert summary["automatic_recall"] is False + + +def _configure_prompt_ingest_binding( + registry_path: Path, + *, + config_automatic_ingest: bool, + policy_automatic_ingest: bool = True, + config_automatic_recall: bool = False, + policy_automatic_recall: bool = False, + receipt_status: str = "verified", + receipt_provider_id: str = "openviking", + drift_config_after_binding: bool = False, + omit_receipt: bool = False, +) -> tuple[dict[str, Any], Path]: + registry = json.loads(registry_path.read_text(encoding="utf-8")) + goal = registry["goals"][0] + goal["state_file"] = "STATE.md" + (Path(goal["repo"]) / "STATE.md").write_text("# Synthetic Goal\n", encoding="utf-8") + policy = goal["control_plane"]["reward_memory"] + config_path = Path(goal["repo"]) / policy["config_path"] + config = json.loads(config_path.read_text(encoding="utf-8")) + config["automation"]["automatic_ingest"] = config_automatic_ingest + config["automation"]["automatic_recall"] = config_automatic_recall + config_path.write_text(json.dumps(config), encoding="utf-8") + digest = "sha256:" + hashlib.sha256(config_path.read_bytes()).hexdigest() + policy["config_digest"] = digest + policy["automation"] = { + "automatic_recall": policy_automatic_recall, + "automatic_ingest": policy_automatic_ingest, + "fail_open": True, + } + if omit_receipt: + policy["enablement_receipts"].pop("pilot", None) + else: + receipt = policy["enablement_receipts"]["pilot"] + receipt["config_digest"] = digest + receipt["status"] = receipt_status + receipt["provider_id"] = receipt_provider_id + if drift_config_after_binding: + config_path.write_bytes(config_path.read_bytes() + b"\n") + registry_path.write_text(json.dumps(registry), encoding="utf-8") + return goal, config_path + + +@pytest.mark.parametrize( + ("agent_id", "config_ingest", "policy_ingest", "receipt_status", "receipt_provider", "drift", "omit_receipt", "expected"), + [ + ("pilot", True, True, "verified", "openviking", False, False, True), + ("meta", True, True, "verified", "openviking", False, False, False), + ("pilot", True, True, "pending", "openviking", False, False, False), + ("pilot", True, True, "verified", "other-provider", False, False, False), + ("pilot", True, True, "verified", "openviking", True, False, False), + ("pilot", False, True, "verified", "openviking", False, False, False), + ("pilot", True, True, "verified", "openviking", False, True, False), + ], +) +def test_heartbeat_prompts_require_effective_agent_ingest( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + agent_id: str, + config_ingest: bool, + policy_ingest: bool, + receipt_status: str, + receipt_provider: str, + drift: bool, + omit_receipt: bool, + expected: bool, +) -> None: + from loopx.upgrade import goal_heartbeat_prompt + + registry_path, _, _ = _experiment(tmp_path) + goal, _ = _configure_prompt_ingest_binding( + registry_path, + config_automatic_ingest=config_ingest, + policy_automatic_ingest=policy_ingest, + receipt_status=receipt_status, + receipt_provider_id=receipt_provider, + drift_config_after_binding=drift, + omit_receipt=omit_receipt, + ) + + upgraded = goal_heartbeat_prompt( + goal, cli_bin="loopx", mode="thin", agent_id=agent_id, + ) + code = main([ + "--registry", str(registry_path), + "--runtime-root", str(tmp_path / "runtime"), + "--format", "json", "heartbeat-prompt", + "--goal-id", goal["id"], "--agent-id", agent_id, + "--codex-app", "--thin", + ]) + packet = json.loads(capsys.readouterr().out) + assert code == 0 and packet["ok"] is True + actual = ( + "--reward-memory-reflection-json" in upgraded["task_body"], + "--reward-memory-reflection-json" in packet["task_body"], + ) + assert actual == (expected, expected) + + +@pytest.mark.parametrize( + ("agent_id", "expected_error"), + [ + (None, "identity-aware peer heartbeat prompt required"), + ("a" * 300, "public-safe token"), + ("ghost", "not registered"), + ], +) +def test_upgrade_reward_memory_prompt_keeps_identity_errors( + tmp_path: Path, agent_id: str | None, expected_error: str +) -> None: + from loopx.upgrade import goal_heartbeat_prompt + + registry_path, _, _ = _experiment(tmp_path) + goal, _ = _configure_prompt_ingest_binding( + registry_path, config_automatic_ingest=True, + ) + + with pytest.raises(ValueError, match=expected_error): + goal_heartbeat_prompt(goal, cli_bin="loopx", mode="thin", agent_id=agent_id) + + +def test_reward_memory_prompt_fails_closed_for_legacy_registration_alias( + tmp_path: Path, +) -> None: + from loopx.upgrade import goal_heartbeat_prompt + + registry_path, _, _ = _experiment(tmp_path) + goal, _ = _configure_prompt_ingest_binding( + registry_path, config_automatic_ingest=True, + ) + goal["registered_agents"] = goal["coordination"].pop("registered_agents") + + payload = goal_heartbeat_prompt( + goal, cli_bin="loopx", mode="thin", agent_id="pilot", + ) + assert "--reward-memory-reflection-json" not in payload["task_body"] + + +def test_heartbeat_prompt_admission_never_runs_provider_preflight( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + from loopx.capabilities.reward_memory import experiment + from loopx.upgrade import goal_heartbeat_prompt + + registry_path, _, _ = _experiment(tmp_path) + goal, _ = _configure_prompt_ingest_binding( + registry_path, config_automatic_ingest=True, + ) + monkeypatch.setattr( + experiment, + "preflight_reward_memory_experiment_config", + lambda *_args, **_kwargs: pytest.fail("prompt admission must not contact provider"), + ) + + payload = goal_heartbeat_prompt( + goal, cli_bin="loopx", mode="thin", agent_id="pilot", + ) + assert "--reward-memory-reflection-json" in payload["task_body"] + + +def test_disabled_reward_memory_prompt_keeps_builder_bytes_and_skips_binding_read( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + from loopx.capabilities.reward_memory import configuration + from loopx.upgrade import goal_heartbeat_prompt + + registry_path, _, _ = _experiment(tmp_path) + goal, _ = _configure_prompt_ingest_binding( + registry_path, + config_automatic_ingest=True, + policy_automatic_ingest=False, + ) + monkeypatch.setattr( + configuration, + "resolve_goal_reward_memory_experiment", + lambda **_kwargs: pytest.fail("disabled policy must not read the binding"), + ) + + payload = goal_heartbeat_prompt( + goal, cli_bin="loopx", mode="thin", agent_id="pilot", + ) + assert "--reward-memory-reflection-json" not in payload["task_body"] + assert payload["interface_budget"]["reward_memory_headroom_chars"] == 0 + + goal_without_reward_memory = json.loads(json.dumps(goal)) + goal_without_reward_memory["control_plane"].pop("reward_memory") + without_policy = goal_heartbeat_prompt( + goal_without_reward_memory, + cli_bin="loopx", + mode="thin", + agent_id="pilot", + ) + assert payload["task_body"] == without_policy["task_body"] + assert payload["interface_budget"] == without_policy["interface_budget"] + + cli_argv = [ + "--registry", str(registry_path), + "--runtime-root", str(tmp_path / "runtime"), + "--format", "json", "heartbeat-prompt", + "--goal-id", goal["id"], "--agent-id", "pilot", "--codex-app", "--thin", + ] + assert main(cli_argv) == 0 + with_reward_memory = json.loads(capsys.readouterr().out) + registry = json.loads(registry_path.read_text(encoding="utf-8")) + registry["goals"][0]["control_plane"].pop("reward_memory") + registry_path.write_text(json.dumps(registry), encoding="utf-8") + assert main(cli_argv) == 0 + without_reward_memory = json.loads(capsys.readouterr().out) + assert with_reward_memory["task_body"] == without_reward_memory["task_body"] + assert with_reward_memory["interface_budget"] == without_reward_memory["interface_budget"] + + +@pytest.mark.parametrize( + ("agent_id", "expected_error"), + [ + (None, "identity-aware peer heartbeat prompt required"), + ("a" * 300, "public-safe registered agent id"), + ("ghost", "not registered"), + ], +) +def test_cli_reward_memory_prompt_keeps_identity_errors( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + agent_id: str | None, + expected_error: str, +) -> None: + registry_path, _, _ = _experiment(tmp_path) + _configure_prompt_ingest_binding( + registry_path, config_automatic_ingest=True, + ) + argv = [ + "--registry", str(registry_path), + "--runtime-root", str(tmp_path / "runtime"), + "--format", "json", "heartbeat-prompt", + "--goal-id", "reward-memory-goal", "--codex-app", "--thin", + ] + if agent_id is not None: + argv.extend(("--agent-id", agent_id)) + code = main(argv) + output = capsys.readouterr().out + assert code != 0 + assert expected_error in output