Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions loopx/cli_commands/project_lifecycle_refresh_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
capture_first_party_host_goal_ref,
)
from ..control_plane.goals.source_session_registry_state import exact_goal_ref
from ..control_plane.goals.vision_checkpoint import prepare_vision_refresh
from ..control_plane.quota.settlement import (
attach_settlement_progress,
read_heartbeat_settlement,
Expand Down Expand Up @@ -515,6 +516,18 @@ def handle_refresh_state_command(
turn_instance_id=args.turn_instance_id,
),
)
if outcome_update is not None and agent_vision_packet is not None:
# Reject malformed input before committing the independent outcome
# refinement. Reuse the typed owner; do not authorize a transition
# or replace the original packet with this context-free result.
# Refresh still checks merged history and policy against current
# state, including whether a terminal stride is actually enabled.
prepare_vision_refresh(
agent_vision_packet, goal_id=args.goal_id, agent_id=args.agent_id,
existing_agent_vision=None, merge_patch=False,
require_path_delta_for_durable_change=False,
allow_terminal_stride_path=True,
)
progress_observation = inline_progress_observation(args)
reward_memory_reflection_json = str(
getattr(args, "reward_memory_reflection_json", None) or ""
Expand Down
42 changes: 42 additions & 0 deletions tests/control_plane/test_goal_outcome_refinement.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,48 @@ def admission():
assert declaration["execution_authority_granted"] is False


@pytest.mark.parametrize("invalid_field", ["evidence_refs", "acceptance_summary", "agent_id"])
def test_batched_refinement_rejects_invalid_vision_before_mutation(acceptance_goal, invalid_field):
project, _, cli, run = acceptance_goal
before = cli("inspect")[1]
update = project / "update.json"
update.write_text(json.dumps({
"operation_id": "validated-writeback",
"expected_provider_revision": before["provider_revision"],
"refinement": refinement([{"id": "opening", "description": "Keep the introduction"}], "message:first"),
}))
packet = {
"state": "vision_patch_proposed",
"vision_patch": {"acceptance_summary": "The introduction still needs validation."},
"path_delta": {"outcome": "continue", "prior_assumption": "Retain the introduction.",
"observed_reality": "Independent validation remains open.",
"retained": ["Original content"], "evidence_refs": ["artifact:tour"]},
}
invalid = json.loads(json.dumps(packet))
if invalid_field == "evidence_refs":
invalid["path_delta"]["evidence_refs"] = [f"artifact:part-{i}" for i in range(5)]
elif invalid_field == "acceptance_summary":
invalid["vision_patch"]["acceptance_summary"] = "x" * 421
else:
invalid["agent_id"] = "other-agent"
vision = project / "vision.json"
vision.write_text(json.dumps(invalid))
args = ("refresh-state", "--goal-id", "goal-acceptance", "--agent-id", "agent-a",
"--no-global-sync", "--goal-outcome-update-json", str(update), "--agent-vision-json", str(vision))
for suffix in [(), ("--dry-run",)]:
code, rejected = run(*args, *suffix)
assert code == 1 and not rejected["appended"], rejected
assert invalid_field in rejected["error"], rejected
assert cli("inspect")[1] == before, "bad input must not advance the outcome or provider revision"
assert "goal_outcome_update" not in rejected
# Repair only the malformed packet: no new basis or operation identity.
vision.write_text(json.dumps(packet))
for status in ["applied", "replayed"]:
code, result = run(*args)
assert code == 0 and result["goal_outcome_update"]["status"] == status, result
assert cli("inspect")[1]["goal_acceptance_contract"]["outcome_requirements"]["revision"] == 1


def test_batched_refinement_receipt_survives_later_writeback_failure(acceptance_goal, monkeypatch, capsys):
from loopx.cli import main
import loopx.cli_commands.project_lifecycle_refresh_state as refresh
Expand Down
Loading