From 4eb06a6260b6ca55f336f39e0c7dfede492d5697 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:59:34 +0800 Subject: [PATCH 01/33] WIP: compose complete Turn source before quota and native capture Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/quota.py | 24 ++- loopx/cli_commands/quota_capture.py | 3 +- loopx/cli_commands/turn.py | 5 +- loopx/control_plane/heartbeat/task_body.py | 39 +--- loopx/control_plane/quota/context_delivery.ts | 2 +- loopx/control_plane/quota/turn_envelope.py | 15 ++ loopx/control_plane/quota/turn_envelope.ts | 75 ++++++++ loopx/control_plane/turn_driver/codex_cli.py | 12 +- .../turn_driver/decision_capture.py | 9 +- loopx/control_plane/turn_driver/executor.py | 6 + .../test_complete_turn_decision.py | 179 ++++++++++++++++++ 11 files changed, 315 insertions(+), 54 deletions(-) create mode 100644 tests/control_plane/test_complete_turn_decision.py diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index 671fa9291b..04c705fa15 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -61,7 +61,7 @@ reconcile_existing_heartbeat_receipt_for_turn, render_existing_heartbeat_receipt_payload, ) -from ..control_plane.quota.turn_envelope import build_turn_envelope +from ..control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision from ..presentation.renderers.quota_event_markdown import ( render_quota_monitor_poll_markdown, render_quota_slot_preview_markdown, @@ -415,6 +415,8 @@ def _project_quota_cli_payload( from ..control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) + if args.quota_command == "should-run" and payload.get("schema_version") == "loopx_turn_envelope_v1": + return payload if not bool(getattr(args, "turn_envelope", False)): if args.quota_command in {"status", "plan"}: return compact_quota_plan_cli_payload(payload, detail_sections=detail_sections) @@ -485,6 +487,15 @@ def _emit_quota_result( payload = {**payload, "replan_read_path": str(capture_directory / "decision.json"), "replan_read_argv": current_loopx_cli_argv()} + if args.quota_command == "should-run" and context is not None: + attach_host_poll_receipt( + context.status_payload, + args, + payload, + registry_path=registry_path, + ) + if payload.get("ok") is True and args.quota_command == "should-run" and isinstance(payload.get("interaction_contract"), Mapping): + payload = compose_turn_decision(payload, scheduler_execution_context=context.scheduler_context if context is not None else None) try: capture_decision(capture_directory, payload) if payload.get("ok") is True: @@ -511,13 +522,6 @@ def _emit_quota_result( str(capture_directory / "decision.json") if capture_directory is not None else None ), ) - if args.quota_command == "should-run" and context is not None: - attach_host_poll_receipt( - context.status_payload, - args, - payload, - registry_path=registry_path, - ) succeeded = bool(payload.get("ok")) # Handoff releases the immutable original before any presentation. Native # actual-input observation owns its projection when explicitly selected; @@ -528,7 +532,7 @@ def _emit_quota_result( payload = project_captured_context(payload, directory=capture_directory, request=delivery_request[0], capture_id=delivery_request[1], goal_id=decision_identity[0], agent_id=decision_identity[1], - surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet") + surface="complete_turn" if captured_payload.get("schema_version") == "loopx_turn_envelope_v1" else ("turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet")) if (args.format == "json" and succeeded and capture_directory and native_context): import hashlib @@ -540,7 +544,7 @@ def _emit_quota_result( root=getattr(args, "codex_context_root", None), create=True) payload = project_codex_context(payload, directory=directory, capture_directory=capture_directory, goal_id=decision_identity[0], agent_id=decision_identity[1], - surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet", + surface="complete_turn" if captured_payload.get("schema_version") == "loopx_turn_envelope_v1" else ("turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet"), capture_id=capture_id, continuity_generation=getattr(args, "_context_delivery_generation", None)) recovery = payload.get("recovery") if recovery is None: diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index cafd14fdd1..c607f7cd42 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -237,7 +237,8 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: raise ValueError("saved projection needs a private physical capture root owned by this user") cache = Path(tempfile.mkdtemp(prefix="context-", dir=root)) projected = project_codex_context(payload, directory=directory, capture_directory=cache, - goal_id=args.goal_id, agent_id=args.agent_id, surface="quota_packet", + goal_id=args.goal_id, agent_id=args.agent_id, + surface="complete_turn" if payload.get("schema_version") == "loopx_turn_envelope_v1" else "quota_packet", capture_id=hashlib.sha256(raw).hexdigest(), continuity_generation=getattr(args, "_context_delivery_generation", None)) native_prepared = True diff --git a/loopx/cli_commands/turn.py b/loopx/cli_commands/turn.py index aa61b7740c..3285fd2421 100644 --- a/loopx/cli_commands/turn.py +++ b/loopx/cli_commands/turn.py @@ -12,7 +12,7 @@ ) from ..control_plane.operator_provider import operator_provider_environ from ..control_plane.quota.effective_action import EffectiveAction -from ..control_plane.quota.turn_envelope import build_turn_envelope +from ..control_plane.quota.turn_envelope import build_turn_envelope, compose_turn_decision from ..control_plane.runtime.status_projection_cache import ( resolve_status_projection_cache_runtime_root, ) @@ -231,7 +231,7 @@ def handle_turn_command( ) operator_inbox_urgency_projector = decision_owner.operator_inbox_urgency_projector scheduler_context = decision_owner.scheduler_execution_context - decision = decision_owner.resolve() + decision = compose_turn_decision(decision_owner.resolve(), scheduler_execution_context=scheduler_context) resume_requested, session_binding = resolve_turn_resume_session_binding(args) turn_envelope = build_turn_envelope( decision, @@ -275,6 +275,7 @@ def handle_turn_command( session_scope=session_scope, goal_ref=goal_ref, ) + payload["work_contract"] = decision # Resolve machine authentication once for the readback and host launch. operator_environ = operator_provider_environ() payload["managed_executor"] = managed_executor_cli_binding(args, environ=operator_environ) diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index 65e737b803..489b0e05cf 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -669,42 +669,8 @@ def render_thin_heartbeat_task_body( if pr_review_pre_quota_command else "" ) - if captured_envelope: - return f"""Advance `{goal_id}` from {active_state}. - -{HOST_LOOP_SAFETY_RULE} -{scope_sentence} -Use TurnEnvelope: `action.must_attempt` requires work; `user.notify` controls -output only. ok=false -> recovery, no delivery. Language=user. Honor `boundary`, `execution_policy`, `contract_capsule`, -`work_context`, remaining `required_reads`, `agent_context`, and `replan_action_packet` when present. -When present, read inline `agent_context` before planning. Read `detail_ref.full_decision` for -selection, replan, missing/truncated commands or an `agent_context.detail_ref`: -same-invocation observation, not fresh admission. -Selection uses the saved `interaction_contract.cli_channel.selection_command`. -Keep all supplied guard transport flags (--turn-envelope, --decision-output-root, -and --decision-handoff-socket when present) on every fresh selection/repair/replan -guard; each call saves its own immutable observation. -After a workspace or state change, re-enter the guard before continuing work. - -```sh -{render_turn_bootstrap()} -{pr_review_pre_quota_instruction}{quota_guard_command} -``` -{SCOPE_BOUNDED_WORK_RULE} -Observed capabilities -> --available-capability, never a user gate. -Apply `scheduler` as projected using inline ACK argv. Read saved scheduler detail -only for a needed missing/truncated/referenced command; failure argv only after host update failure. -Exact monitor settlement=no refresh/spend; auxiliary poll continues work. -Validate work; execute `writeback.settlement_plan` and `writeback.next_cli_actions` with -their exact identity. Do not guess commands from abbreviated previews. -Writeback needs outcome and vision; unchanged reason requires an existing -current evidence-linked vision. Missing vision after writeback -> same-Turn -checkpoint-context then evidence; stale -> reread. -{HOST_LOOP_TODO_CLOSEOUT_COMPACT_RULE} -P0 blocked -> admitted P1/P2; quiet polls do not spend. -`agent_read_required`: read/triage before work, settle/ACK. -{reward_memory_rule} -{policy_tail}""" + # New captures use one complete contract; the old caller option no longer + # chooses a separate execution view. Historical saved bodies remain intact. return f"""Advance `{goal_id}` from {active_state}. {RUNTIME_EXECUTION_ROUTING_RULE} @@ -712,6 +678,7 @@ def render_thin_heartbeat_task_body( {scope_sentence} {HOST_LOOP_QUOTA_DISPATCH_RULE} +Preserve the supplied capture/handoff flags on selection and same-Turn repair; read the complete captured contract before acting. {HEARTBEAT_TURN_BOOTSTRAP_RULE} ```sh {render_turn_bootstrap()} diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index e31d1277a1..d171f36cf1 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -146,7 +146,7 @@ function splitContext(context: JsonObject): Split { /** The projection surface is a transport binding, never a new admission mode. * Keep one exact codec for quota and Turn; source-only callers use the same codec. */ -const SURFACES = ["work_context", "quota_packet", "turn_envelope"] as const; +const SURFACES = ["work_context", "quota_packet", "turn_envelope", "complete_turn"] as const; type Surface = typeof SURFACES[number]; type Delta = {value: unknown} | {from: Path} | {changed?: Record; values?: Record; removed?: string[]} | {items: Record; values?: Record; length?: number}; diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 4179d8b806..1befea4e6f 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -76,6 +76,15 @@ def turn_envelope_replan_settlement_identity( return dict(identity) +def compose_turn_decision( + payload: Mapping[str, Any], *, scheduler_execution_context: object = None, +) -> dict[str, Any]: + """Transport complete source facts to the existing typed composition owner.""" + resolution = resolve_scheduler_execution_context(scheduler_execution_context) + args = render_scheduler_execution_args(scheduler_execution_context=resolution) if resolution.ok and resolution.context is not None else "" + return _turn_envelope_result("compose_complete", payload=dict(payload), scheduler_execution_args=args) + + def build_turn_envelope( payload: Mapping[str, Any], *, @@ -92,6 +101,12 @@ def build_turn_envelope( scheduler_execution_args = render_scheduler_execution_args( scheduler_execution_context=resolution, ) + # A captured complete contract already records the original host binding. + # Only transport that value when no caller override was supplied; TS checks + # its integrity and rejects an explicit conflicting binding. + if scheduler_execution_context is None and payload.get("schema_version") == "loopx_turn_envelope_v1": + binding = payload.get("contract_binding") + scheduler_execution_args = binding.get("scheduler_execution_args") if isinstance(binding, Mapping) else None return _turn_envelope_result( "build", payload=dict(payload), diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 6373bf9fb3..1d89a5f43e 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1147,6 +1147,63 @@ function coldPath( }; } +/** Complete decision composition precedes capture. Domain owners have already + * decided admission, work and settlement; this boundary does not decide again. + * The v0 envelope remains an internal executor adapter during reader migration. + */ +export const COMPLETE_TURN_DECISION_VERSION = "loopx_turn_envelope_v1"; + +export function requireCompleteTurnDecision(value: unknown): JsonObject { + const decision = requireJsonObject(value, "complete Turn decision"); + if (decision.schema_version !== COMPLETE_TURN_DECISION_VERSION) { + throw new EffectRuntimeRequestError("unsupported complete Turn decision version"); + } + const binding = requireJsonObject(decision.contract_binding, "Turn contract binding"); + requireJsonObject(decision.interaction_contract, "Turn interaction contract"); + if (typeof binding.scheduler_execution_args !== "string") { + throw new EffectRuntimeRequestError("Turn scheduler execution binding must be a string"); + } + const unsignedBinding = {...binding}; + delete unsignedBinding.content_hash; + const content = {...decision, contract_binding: unsignedBinding}; + if (binding.content_hash !== canonicalHash(content)) { + throw new EffectRuntimeRequestError("complete Turn decision content integrity mismatch"); + } + const source = {...decision}; + delete source.schema_version; + delete source.contract_binding; + if (binding.source_decision_hash !== canonicalHash(source)) { + throw new EffectRuntimeRequestError("complete Turn decision source integrity mismatch"); + } + assertSupportedQuotaPacket(source); + return decision; +} + +export function composeCompleteTurnDecision(value: unknown): JsonObject { + const request = requireJsonObject(value, "complete Turn decision request"); + const payload = requireJsonObject(request.payload, "complete Turn decision source"); + if (typeof request.scheduler_execution_args !== "string") { + throw new EffectRuntimeRequestError("Turn scheduler execution binding must be a string"); + } + if (payload.schema_version === COMPLETE_TURN_DECISION_VERSION) { + const prior = requireCompleteTurnDecision(payload); + if (object(prior.contract_binding).scheduler_execution_args !== request.scheduler_execution_args) { + throw new EffectRuntimeRequestError("complete Turn decision cannot change its host binding"); + } + return prior; + } + if (Object.hasOwn(payload, "schema_version") || Object.hasOwn(payload, "contract_binding")) { + throw new EffectRuntimeRequestError("complete Turn decision source contains reserved version/binding fields"); + } + assertSupportedQuotaPacket(payload); + requireJsonObject(payload.interaction_contract, "Turn interaction contract"); + const result: JsonObject = {...payload, schema_version: COMPLETE_TURN_DECISION_VERSION, + contract_binding: {scheduler_execution_args: request.scheduler_execution_args, + source_decision_hash: canonicalHash(payload)}}; + result.contract_binding = {...object(result.contract_binding), content_hash: canonicalHash(result)}; + return result; +} + export function buildTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const payload = requireJsonObject(request.payload, "turn envelope payload"); @@ -1155,6 +1212,12 @@ export function buildTurnEnvelope(value: unknown): JsonObject { "turn envelope scheduler_execution_args must be a string", ); } + if (payload.schema_version === COMPLETE_TURN_DECISION_VERSION) { + requireCompleteTurnDecision(payload); + if (object(payload.contract_binding).scheduler_execution_args !== request.scheduler_execution_args) { + throw new EffectRuntimeRequestError("Turn adapter host binding differs from captured contract"); + } + } const schedulerExecutionArgs = request.scheduler_execution_args; const capturedDecisionPath = request.captured_decision_path; if (capturedDecisionPath !== undefined && ( @@ -1275,6 +1338,18 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { autonomous_replan_obligation: {...obligation, context_read: reads.review}, replan_action_packet: replanActionPacket(payload)}; } + if (operation === "compose_complete") return composeCompleteTurnDecision(request); + if (operation === "validate_complete") { + const decision = requireCompleteTurnDecision(request.payload); + const envelope = requireJsonObject(request.envelope, "complete Turn executor envelope"); + const signature = object(envelope.action_signature); + if (signature.source_decision_hash !== canonicalHash(decision) + || signature.envelope_hash !== canonicalHash(turnEnvelopeActionSignatureDocument(envelope)) + || signature.source_hash !== signature.envelope_hash || signature.matches !== true) { + throw new EffectRuntimeRequestError("complete Turn model input differs from captured executor source"); + } + return decision; + } if (operation === "build") return buildTurnEnvelope(request); if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request); diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index bd88af8dce..0f29974994 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -310,7 +310,13 @@ def _prompt(request: Mapping[str, Any]) -> str: envelope = _mapping(request.get("turn_envelope")) staged = _mapping(_mapping(envelope.get("detail_ref")).get("replan_action_packet")).get("review") model_request = request - if staged: + complete = request.get("work_contract") + if complete is not None: + from ..quota.turn_envelope import _turn_envelope_result + + _turn_envelope_result("validate_complete", payload=complete, envelope=envelope) + model_request = {key: value for key, value in request.items() if key != "turn_envelope"} + if staged and complete is None: from .host_candidate import extract_turn_authority, replan_prompt_instruction authority = extract_turn_authority(request) @@ -323,7 +329,7 @@ def _prompt(request: Mapping[str, Any]) -> str: ) instructions = [ "Execute exactly one bounded LoopX Turn in the current workspace.", - ("Use turn_authority as the verified authority for this Turn. " if staged else "Use the TurnEnvelope as the source of truth. ") + ("Use work_contract as the complete source of truth. Its interaction_contract.agent_channel carries work_context, required_reads and primary_action; its cli_channel describes Core effects owned by the outer adapter. " if complete is not None else ("Use turn_authority as the verified authority for this Turn. " if staged else "Use the TurnEnvelope as the source of truth. ")) + "Read work_context and remaining required_reads before primary_action; do not repeat fulfilled reads; perform work only when its contract allows it. The selected Todo summary is not its full requirements, and completing that work does not prove Goal completion.", "Do not write LoopX state, spend quota, or apply scheduler changes; the adapter owns those effects.", "Return only the schema-constrained result. For validated_progress, repair_required, or replan_required, fill every material field with public-safe evidence.", @@ -334,7 +340,7 @@ def _prompt(request: Mapping[str, Any]) -> str: "For user_action_required, wait, or iteration_failed, leave material-only fields empty and explain the stop in summary. iteration_failed ends only this iteration and never requests a retry or successor.", 'completed_phases must be exactly ["host_execute","typed_result"], and turn_key must match the request.', ] - if staged: + if staged and complete is None: instructions.append(replan_prompt_instruction(authority)) instructions.extend(request.get("capability_instructions") or ()) boundary = _mapping(_mapping(request.get("turn_envelope")).get("boundary")) diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index e127e6a16b..798da76536 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -74,7 +74,14 @@ def prepare_turn_decision_capture( if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) write_decision_capture(path, observation) - return {**plan, "turn_envelope": bound} + restored = {**plan, "turn_envelope": bound} + if observation.get("schema_version") == "loopx_turn_envelope_v1": + restored["work_contract"] = observation + else: + # Historical observations retain their original input contract. Never + # pair an old capture with a freshly observed complete decision. + restored.pop("work_contract", None) + return restored def validate_handoff_socket(value: str) -> Path: diff --git a/loopx/control_plane/turn_driver/executor.py b/loopx/control_plane/turn_driver/executor.py index 422440c36b..b628a958e2 100644 --- a/loopx/control_plane/turn_driver/executor.py +++ b/loopx/control_plane/turn_driver/executor.py @@ -169,6 +169,8 @@ def build_loopx_turn_host_request(plan: Mapping[str, Any]) -> dict[str, Any]: "stdout": "one public-safe JSON object", }, } + if isinstance(plan.get("work_contract"), Mapping): + request["work_contract"] = dict(plan["work_contract"]) goal_ref = plan.get("goal_ref") if isinstance(goal_ref, Mapping): request["goal_ref"] = dict(goal_ref) @@ -1620,6 +1622,10 @@ def persist_source_journal(source_admission: Mapping[str, Any]) -> None: # Preserve any session recovery adjustment; only the original # observation replaces the fresh request's context. request = {**request, "turn_envelope": plan["turn_envelope"]} + if isinstance(plan.get("work_contract"), Mapping): + request["work_contract"] = dict(plan["work_contract"]) + else: + request.pop("work_contract", None) admission = None if needs_host and admit_start is not None: admission = admit_start( diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py new file mode 100644 index 0000000000..5fdbd55472 --- /dev/null +++ b/tests/control_plane/test_complete_turn_decision.py @@ -0,0 +1,179 @@ +"""Candidate complete source through actual producers; no model is invoked.""" +import copy +import json +import sys + +import pytest + +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, compose_turn_decision, + prepare_context_delivery, restore_context_delivery, +) +from loopx.control_plane.turn_driver.codex_cli import _prompt +from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request +from tests.control_plane.test_quota_settlement_cli import ( + _write_fixture, _configure_read_only_todo, _run_cli, _spend_run_count, + GOAL_ID, AGENT_ID, TODO_ID, +) +from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime + + +def test_complete_source_rejects_tampering_and_binding_substitution(): + source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, + "interaction_contract": {}, "future_evidence": {"original": "unchanged"}} + before = copy.deepcopy(source) + result = compose_turn_decision(source) + assert {k: v for k, v in result.items() if k not in {"schema_version", "contract_binding"}} == before + assert source == before + assert compose_turn_decision(result) == result + tampered = copy.deepcopy(result) + tampered["future_evidence"]["original"] = "changed" + with pytest.raises(RuntimeError, match="integrity mismatch"): + compose_turn_decision(tampered) + with pytest.raises(RuntimeError, match="host binding"): + _turn_envelope_result("compose_complete", payload=result, scheduler_execution_args="different") + with pytest.raises(RuntimeError, match="reserved"): + compose_turn_decision({**source, "schema_version": "unknown"}) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeypatch, provider): + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + state = _configure_read_only_todo(project) + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, read_fixture_todos + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state)), + state_path=state, provider=provider) + capture = tmp_path / "capture" + code, output = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "complete-source", + "--codex-app", "--available-capability", "shell", "--available-capability", "filesystem_read", + "--decision-output-dir", str(capture), "--turn-envelope") + assert code == 0, output + original = (capture / "decision.json").read_bytes() + contract = json.loads(original) + assert contract["schema_version"] == "loopx_turn_envelope_v1" + assert output["interaction_contract"] == contract["interaction_contract"] + assert output["contract_binding"] == contract["contract_binding"] + assert "action" not in output and "contract_capsule" not in output + scope = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id="fixture", context_epoch="one") + first = prepare_context_delivery(contract, scope=scope, capture_id="one", surface="complete_turn") + current = prepare_context_delivery(contract, scope=scope, capture_id="two", surface="complete_turn") + warm = prepare_context_delivery(contract, scope=scope, capture_id="two", surface="complete_turn", + baseline=first, retained={"complete": True, "target": first["target"]}) + assert restore_context_delivery(warm, baseline=first, current=current)["context"] == contract + assert restore_context_delivery(warm, baseline=None, current=current)["context"] == contract + old = prepare_context_delivery(contract, scope=scope, capture_id="one", surface="quota_packet") + changed_surface = prepare_context_delivery(contract, scope=scope, capture_id="two", surface="complete_turn", + baseline=old, retained={"complete": True, "target": old["target"]}) + assert changed_surface["mode"] == "full" + assert (capture / "decision.json").read_bytes() == original + assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("first_attempt_fails", [False, True]) +def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, provider, first_attempt_fails): + from tests.control_plane.test_turn_host_decision_capture import _fixture_module + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority + isolate_sqlite_runtime(tmp_path, monkeypatch) + f = _fixture_module() + project, runtime, workspace, registry = f._write_fixture(tmp_path, turn_count=2) + state = project / ".codex/goals" / f.GOAL_ID / "ACTIVE_GOAL_STATE.md" + summary = parse_active_state_todos(state.read_text(), state_path=state, item_limit=None) + initialize_canonical_authority(runtime, f.GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=f.GOAL_ID, handoff_mode="soft_claim", + todos=summary["agent_todos"]["items"]), state_path=state, provider=provider) + args = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", + "turn", "plan", "--goal-id", f.GOAL_ID, "--agent-id", f.AGENT_ID, + "--turn-instance-id", "complete-source", "--host", "codex-cli", + "--execution-mode", "isolated-headless", "--include-transaction-detail"] + code, plan = f._run_cli(args) + assert code == 0, plan + assert "work_contract" in plan, json.dumps(plan) + contract = plan["work_contract"] + assert contract["schema_version"] == "loopx_turn_envelope_v1" + request = build_loopx_turn_host_request(plan) + prompt = _prompt(request) + actual = json.loads(prompt.split("Turn request:\n", 1)[1]) + assert actual["work_contract"] == contract + assert "turn_envelope" not in actual and "turn_authority" not in actual + tampered = copy.deepcopy(request) + tampered["work_contract"]["reason"] = "different facts" + with pytest.raises(RuntimeError, match="integrity mismatch"): + _prompt(tampered) + assert not list(runtime.rglob("decisions/*.json")) + assert f._quota_spend_count(runtime) == 0 + + # An old journal must not inherit a new admission's complete source. + from loopx.control_plane.turn_driver.decision_capture import prepare_turn_decision_capture + historical = {key: value for key, value in contract.items() + if key not in {"schema_version", "contract_binding"}} + historical_envelope = _turn_envelope_result("build", payload=historical, + scheduler_execution_args=contract["contract_binding"]["scheduler_execution_args"]) + historical_plan = {**plan, "turn_envelope": historical_envelope} + historical_journal = tmp_path / "historical" / "journal.json" + prepared = prepare_turn_decision_capture(historical_plan, journal_path=historical_journal, + decision=historical, existing_journal=False) + restored = prepare_turn_decision_capture({**prepared, "work_contract": contract}, + journal_path=historical_journal, decision=None, existing_journal=True) + assert "work_contract" not in restored + assert "work_contract" not in build_loopx_turn_host_request(restored) + + executable = f._write_fake_codex(tmp_path) + script = executable.read_text().replace("#!/usr/bin/env python3", "#!" + sys.executable) + script = script.replace("prompt = sys.stdin.read()", 'prompt = sys.stdin.read()\npathlib.Path("received.txt").write_text(prompt)') + if first_attempt_fails: + script = script.replace("marker = pathlib.Path(", + 'failed = pathlib.Path("failed-once")\n' + 'if not failed.exists():\n' + ' failed.touch()\n' + ' print(json.dumps({"type":"thread.started","thread_id":"session-fixture-0001"}), flush=True)\n' + ' print("Selected model is at capacity. Please try a different model.", file=sys.stderr)\n' + ' raise SystemExit(9)\n' + 'marker = pathlib.Path(') + executable.write_text(script) + run_args = f._base_argv(registry=registry, runtime=runtime, workspace=workspace, + codex_bin=executable, model=None, timeout_seconds=20, + expected_marker=f._marker_value(1), turn_instance_id="complete-execution") + code, executed = f._run_cli([*run_args, "--execute"]) + if first_attempt_fails: + assert code == 1 and executed["status"] == "failed", executed + assert f._quota_spend_count(runtime) == 0 + else: + assert code == 0 and executed["status"] == "committed", executed + observed = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) + assert observed["work_contract"]["schema_version"] == "loopx_turn_envelope_v1" + assert "turn_envelope" not in observed + saved = list(runtime.rglob("decisions/*.json")) + assert len(saved) == 1 + saved_bytes = saved[0].read_bytes() + assert json.loads(saved_bytes) == observed["work_contract"] + replay_args = list(run_args) + index = replay_args.index("--turn-instance-id") + del replay_args[index:index + 2] + if first_attempt_fails: + retry_args = [*replay_args, "--resume-turn-key", observed["turn_key"], "--retry-failed-turn", "--execute"] + received = (workspace / "received.txt").read_bytes() + for corrupt in (True, False): + if corrupt: + saved[0].write_text(json.dumps({**observed["work_contract"], "reason": "tampered source"})) + else: + saved[0].unlink() + code, rejected = f._run_cli(retry_args) + assert code == 1, rejected + assert (workspace / "received.txt").read_bytes() == received + assert f._quota_spend_count(runtime) == 0 + saved[0].write_bytes(saved_bytes) + code, recovered = f._run_cli(retry_args) + assert code == 0 and recovered["status"] == "committed", recovered + restored = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) + assert restored["work_contract"] == observed["work_contract"] + code, replay = f._run_cli([*replay_args, "--resume-turn-key", observed["turn_key"], "--execute"]) + assert code == 0 and replay["replayed"] is True, replay + assert saved[0].read_bytes() == saved_bytes + assert f._quota_spend_count(runtime) == 1 From 9e923fca05ed95d3e347f83a199b1bcc32568369 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:15:28 +0800 Subject: [PATCH 02/33] Preserve canonical capability and replan readers in complete Turn consumers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/ark_managed_agent_host.py | 2 +- loopx/control_plane/heartbeat/task_body.py | 18 +++--- loopx/control_plane/turn_driver/codex_cli.py | 21 +++++++ .../turn_driver/host_candidate.py | 8 ++- .../test_complete_turn_decision.py | 56 +++++++++++++++++++ .../test_quota_settlement_cli.py | 4 +- tests/test_ark_managed_agent_host.py | 2 +- 7 files changed, 93 insertions(+), 18 deletions(-) diff --git a/loopx/ark_managed_agent_host.py b/loopx/ark_managed_agent_host.py index a31e9932d1..35aae70589 100644 --- a/loopx/ark_managed_agent_host.py +++ b/loopx/ark_managed_agent_host.py @@ -49,7 +49,7 @@ def build_ark_managed_agent_host_contract() -> dict[str, Any]: "quota_should_run.interaction_contract.cli_channel." "runtime_capability_reentry" ), - "cli_projection_ref": "quota_should_run.runtime_capability_reentry", + "cli_projection_ref": "quota_should_run.interaction_contract.cli_channel.runtime_capability_reentry", "packet_schema_version": RUNTIME_CAPABILITY_REENTRY_SCHEMA_VERSION, "delivery_channel": "quota_tool_result", "goal_prompt_mutated": False, diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index 489b0e05cf..d8f3ff6c35 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -63,8 +63,7 @@ def bind_exact_turn_settlement_task_body( placeholder, render_turn_bootstrap(value=turn_instance_id), ) - plan_path = ("writeback.settlement_plan.ordered_steps" if captured_envelope - else "interaction_contract.cli_channel.settlement_plan.ordered_steps") + plan_path = "interaction_contract.cli_channel.settlement_plan.ordered_steps" return ( f"{bound}\n\nFor accountable delivery, execute " f"`{plan_path}` " @@ -537,10 +536,9 @@ def _render_goal_task_body( permission_rule=permission_rule, ) envelope_rule = ( - "For TurnEnvelope, honor action/boundary/required_reads; read " - "detail_ref.full_decision for the complete same-invocation contract " - "before selection, work or settlement. Retain transport flags on fresh guards.\n" - if captured_envelope else "" + "Read the complete captured contract before selection, work or settlement; " + "use interaction_contract for action, required reads and settlement. " + "Retain supplied transport flags on fresh guards.\n" ) if captured_context: # The opted-in native Goal has a 4000-character host limit, including @@ -548,10 +546,8 @@ def _render_goal_task_body( # intact and refer to the registered active state instead of repeating # its path. Ordinary/off Goal instructions remain byte-for-byte intact. envelope_rule = ( - "Read complete same-invocation detail_ref.full_decision before " - "selection/work/settlement; honor envelope action/boundary/required_reads; " - "keep fresh-guard flags.\n" - if captured_envelope else "" + "Read the complete captured contract before selection/work/settlement; " + "keep fresh-guard transport flags.\n" ) wait_rule = ( "\n`/goal` owns blocked state. Recheck quota at " @@ -569,7 +565,7 @@ def _render_goal_task_body( `{quota_guard_command}` {envelope_rule}Follow required selection_command/reentry; do/verify authorized work. Settle current settlement_plan.ordered_steps, else next_cli_actions. Exact identities/effect ids; actual outcomes. Never rebuild static refresh/spend; ambiguous effects: readback/recovery, no blind retry. Keep flags; truthful evidence. Authorized entrypoint repair; unavailable/incomplete contract: no work/spend/completion claim. -Read agent_channel.work_context+required_reads (or envelope) before work. Size by task/evidence/risk within authority/budget, not ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, not wake counts. Continue feasible integration/negatives/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: keep original/cumulative requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. +Read interaction_contract.agent_channel.work_context+required_reads before work. Size by task/evidence/risk within authority/budget, not ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, not wake counts. Continue feasible integration/negatives/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: keep original/cumulative requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. Notify affects output only; progress alone creates no host Goal. After settlement recheck quota; obey continuation/wait, no unchanged polls. Complete {completion_subject} only on should_run=false + terminal no-follow-up; else next action/wait.{wait_rule} {policy_tail}""" diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index 0f29974994..329630cf30 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -316,6 +316,22 @@ def _prompt(request: Mapping[str, Any]) -> str: _turn_envelope_result("validate_complete", payload=complete, envelope=envelope) model_request = {key: value for key, value in request.items() if key != "turn_envelope"} + # Capture routes are host transport metadata, not another decision view. + # Keep the complete captured contract immutable; the internal historical + # adapter supplies only the original, hash-bound material readers. + detail = _mapping(envelope.get("detail_ref")) + captured = _mapping(detail.get("captured_decision")) + if captured: + model_request["source_decision_hash"] = _mapping(envelope.get("action_signature"))["source_decision_hash"] + model_request["detail_ref"] = { + "full_decision": detail["full_decision"], + "captured_decision": {**captured, "source_hash_ref": "$.source_decision_hash"}, + } + if staged: + model_request["detail_ref"]["replan_action_packet"] = { + key: value for key, value in _mapping(detail.get("replan_action_packet")).items() + if key in {"review", "writeback"} + } if staged and complete is None: from .host_candidate import extract_turn_authority, replan_prompt_instruction @@ -342,6 +358,11 @@ def _prompt(request: Mapping[str, Any]) -> str: ] if staged and complete is None: instructions.append(replan_prompt_instruction(authority)) + elif staged: + from .host_candidate import replan_prompt_instruction + + instructions.append(replan_prompt_instruction( + _mapping(model_request.get("detail_ref")), packet_path="detail_ref.replan_action_packet")) instructions.extend(request.get("capability_instructions") or ()) boundary = _mapping(_mapping(request.get("turn_envelope")).get("boundary")) if boundary.get("checkpointed_boundary_authority"): diff --git a/loopx/control_plane/turn_driver/host_candidate.py b/loopx/control_plane/turn_driver/host_candidate.py index d01a976c49..3b544da5b7 100644 --- a/loopx/control_plane/turn_driver/host_candidate.py +++ b/loopx/control_plane/turn_driver/host_candidate.py @@ -178,15 +178,17 @@ def extract_action_text(request: Mapping[str, Any]) -> str: return str(extract_turn_authority(request)["primary_action"]) -def replan_prompt_instruction(authority: Mapping[str, Any]) -> str: +def replan_prompt_instruction( + authority: Mapping[str, Any], *, packet_path: str = "replan_action_packet", +) -> str: """Reader routing shared by native Codex and structured Host transports.""" packet = _mapping(authority.get("replan_action_packet")) if not packet: return "" source = ( - "Read replan_action_packet.review and the applicable acceptance/evidence reads. " + f"Read {packet_path}.review and the applicable acceptance/evidence reads. " "Decide the next work and route from those facts before reading " - "replan_action_packet.writeback for the semantic result format. " + f"{packet_path}.writeback for the semantic result format. " "The readers verify the original source hash; check their Goal/Agent and " "obligation/context identity against this Turn. " if packet.get("review") else diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 5fdbd55472..ed30e543c4 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -177,3 +177,59 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p assert code == 0 and replay["replayed"] is True, replay assert saved[0].read_bytes() == saved_bytes assert f._quota_spend_count(runtime) == 1 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_native_replan_complete_input_retains_verified_material_readers(tmp_path, monkeypatch, provider): + from tests.control_plane.test_turn_replan_writeback import _fixture + from tests.control_plane.test_quota_settlement_cli import _classification_count + project, runtime, registry, _state, _prior_turn = _fixture(tmp_path, monkeypatch, provider) + turn = "complete-replan-readers" + executable = tmp_path / "replan-host" + executable.write_text("#!" + sys.executable + ''' +import json, pathlib, shlex, subprocess, sys +print(json.dumps({"type":"thread.started", "thread_id":"session-fixture-replan"}), flush=True) +prompt = sys.stdin.read() +root = pathlib.Path.cwd() +(root / "received.txt").write_text(prompt) +request = json.loads(prompt.split("Turn request:\\n", 1)[1]) +packet = request["detail_ref"]["replan_action_packet"] +for stage in ("review", "writeback"): + read = subprocess.run(shlex.split(packet[stage]), text=True, capture_output=True, check=True) + (root / (stage + ".json")).write_text(read.stdout) +output = pathlib.Path(sys.argv[sys.argv.index("--output-last-message") + 1]) +output.write_text(json.dumps({"schema_version":"loopx_turn_result_v0", "turn_key":request["turn_key"], + "result_kind":"wait", "completed_phases":["host_execute","typed_result"], + "summary":"Original material readers verified; no task outcome claimed."})) +''') + executable.chmod(0o700) + code, result = _run_cli(registry, runtime, "turn", "run-once", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--turn-instance-id", turn, "--host", "codex-cli", + "--execution-mode", "isolated-headless", "--project", str(project), + "--codex-bin", str(executable), "--timeout-seconds", "20", "--no-global-sync", "--execute", cwd=project) + assert code == 0, json.dumps(result, indent=2) + prompt = (project / "received.txt").read_text() + request = json.loads(prompt.split("Turn request:\n", 1)[1]) + source = request["work_contract"] + assert source["schema_version"] == "loopx_turn_envelope_v1" + assert "turn_envelope" not in request and "turn_authority" not in request + context = source["autonomous_replan_obligation"]["replan_context"] + review = json.loads((project / "review.json").read_text()) + writeback = json.loads((project / "writeback.json").read_text()) + assert review["context_id"] == writeback["context_id"] == context["context_id"] + assert review["source_hash"] == writeback["source_hash"] == request["source_decision_hash"] + assert "Read detail_ref.replan_action_packet.review" in prompt + assert "before reading detail_ref.replan_action_packet.writeback" in prompt + assert "Never re-enter quota admission" in prompt and "Do not execute writeback commands" in prompt + from pathlib import Path + import shlex + import subprocess + captured = Path(request["detail_ref"]["captured_decision"]["path"]) + original = captured.read_bytes() + assert json.loads(original) == source + captured.write_text(json.dumps({**source, "reason": "Changed original facts"})) + rejected = subprocess.run(shlex.split(request["detail_ref"]["replan_action_packet"]["review"]), + capture_output=True, text=True) + assert rejected.returncode != 0 + captured.write_bytes(original) + assert _classification_count(runtime, "quota_slot_spent") == 0 diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index bfe0fbba30..35c7d697e9 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -4132,8 +4132,8 @@ def test_visible_goal_capability_reentry_preserves_turn_through_selection( assert first["interaction_contract"]["cli_channel"]["selection_required"] is True assert first["selected_todo"]["todo_id"] == TODO_ID assert first["interaction_contract"]["agent_channel"]["next_task_action"]["kind"] == "capability_verification" - assert first["interaction_contract"]["cli_channel"]["next_cli_actions"][0] == first["runtime_capability_reentry"]["candidates"][0]["command"] - reentry_command = first["runtime_capability_reentry"]["candidates"][0][ + assert first["interaction_contract"]["cli_channel"]["next_cli_actions"][0] == first["interaction_contract"]["cli_channel"]["runtime_capability_reentry"]["candidates"][0]["command"] + reentry_command = first["interaction_contract"]["cli_channel"]["runtime_capability_reentry"]["candidates"][0][ "command" ] assert f"--turn-instance-id {turn_instance_id}" in reentry_command diff --git a/tests/test_ark_managed_agent_host.py b/tests/test_ark_managed_agent_host.py index 605b5851cc..36a19017ed 100644 --- a/tests/test_ark_managed_agent_host.py +++ b/tests/test_ark_managed_agent_host.py @@ -126,7 +126,7 @@ def test_goal_prompt_projects_goal_only_host_contract() -> None: "quota_should_run.interaction_contract.cli_channel." "runtime_capability_reentry" ), - "cli_projection_ref": "quota_should_run.runtime_capability_reentry", + "cli_projection_ref": "quota_should_run.interaction_contract.cli_channel.runtime_capability_reentry", "packet_schema_version": "runtime_capability_reentry_v0", "delivery_channel": "quota_tool_result", "goal_prompt_mutated": False, From 01f90443b08fc39e44a96043d0f8c195400d71af Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:22:53 +0800 Subject: [PATCH 03/33] Keep replan observations separate from native launch settlement binding Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/turn_decision.py | 7 +++++-- loopx/cli_commands/turn_run_once.py | 3 +-- tests/control_plane/test_complete_turn_decision.py | 14 +++++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/loopx/cli_commands/turn_decision.py b/loopx/cli_commands/turn_decision.py index 81f79c89c7..c780897d78 100644 --- a/loopx/cli_commands/turn_decision.py +++ b/loopx/cli_commands/turn_decision.py @@ -317,7 +317,7 @@ def build_fresh_turn_decision_owner( def current_turn_host_launch_decision( args: argparse.Namespace, *, registry_path: Path, runtime_root: Path, - identity: SettlementIdentity, replan_obligation_id: str | None, + identity: SettlementIdentity, planned_effective_action: object, scheduler_execution_context: Mapping[str, Any], operator_inbox_urgency_projector: Callable[..., dict[str, Any]], @@ -330,6 +330,9 @@ def current_turn_host_launch_decision( cannot reconstruct it. This read neither creates another receipt nor substitutes a newly selected task into the already planned Host request. """ + # A replan observation may accompany a Todo-bound Turn. Only the recorded + # settlement identity can bind admission to an obligation; passing the + # observed obligation as well would erase the original Todo selection. current = build_live_quota_should_run_decision( collect_turn_status_payload(args, registry_path=registry_path, runtime_root_arg=str(runtime_root)), @@ -341,7 +344,7 @@ def current_turn_host_launch_decision( operator_inbox_urgency_projector=operator_inbox_urgency_projector, bounded_research_frontier_projector=project_live_explore_composition_frontier, receipt_bound_todo_id=identity.todo_id or None, - receipt_bound_replan_obligation_id=replan_obligation_id, + receipt_bound_replan_obligation_id=identity.replan_obligation_id, turn_instance_id=identity.turn_instance_id, turn_start_hook_dispatch=turn_start_hook_dispatch, workspace_path=Path(args.project).expanduser().resolve(), goal_ref=goal_ref, diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index 995c837cdd..b55220cc91 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -846,7 +846,6 @@ def scheduler(_spend_payload: dict[str, object]) -> dict[str, object]: def native_launch_decision(current_registry: Path) -> Mapping[str, Any]: return current_turn_host_launch_decision(args, registry_path=current_registry, runtime_root=runtime_root, identity=settlement_identity, - replan_obligation_id=replan_obligation_id, planned_effective_action=payload["turn_envelope"].get("effective_action"), scheduler_execution_context=payload.get("scheduler_execution_context") or {}, operator_inbox_urgency_projector=operator_inbox_urgency_projector, @@ -870,7 +869,7 @@ def current_launch_decision(current_registry: Path) -> Mapping[str, Any]: # when its mirror still advertises old quota or health. return current_turn_host_launch_decision( args, registry_path=current_registry, runtime_root=runtime_root, - identity=settlement_identity, replan_obligation_id=replan_obligation_id, + identity=settlement_identity, planned_effective_action=payload["turn_envelope"].get("effective_action"), scheduler_execution_context=payload.get("scheduler_execution_context") or {}, operator_inbox_urgency_projector=operator_inbox_urgency_projector, diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index ed30e543c4..9fb22ae61a 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -180,11 +180,12 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_native_replan_complete_input_retains_verified_material_readers(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("prior_todo_binding", [False, True]) +def test_native_replan_complete_input_retains_verified_material_readers(tmp_path, monkeypatch, provider, prior_todo_binding): from tests.control_plane.test_turn_replan_writeback import _fixture - from tests.control_plane.test_quota_settlement_cli import _classification_count + from tests.control_plane.test_quota_settlement_cli import _classification_count, SELECTED_REPLAN_TODO_ID project, runtime, registry, _state, _prior_turn = _fixture(tmp_path, monkeypatch, provider) - turn = "complete-replan-readers" + turn = _prior_turn if prior_todo_binding else "complete-replan-readers" executable = tmp_path / "replan-host" executable.write_text("#!" + sys.executable + ''' import json, pathlib, shlex, subprocess, sys @@ -213,6 +214,13 @@ def test_native_replan_complete_input_retains_verified_material_readers(tmp_path source = request["work_contract"] assert source["schema_version"] == "loopx_turn_envelope_v1" assert "turn_envelope" not in request and "turn_authority" not in request + identity = result["receipt"]["lineage"] + if prior_todo_binding: + assert identity["todo_id"] == SELECTED_REPLAN_TODO_ID + assert not identity.get("replan_obligation_id") + else: + assert not identity["todo_id"] + assert identity["replan_obligation_id"] == source["autonomous_replan_obligation"]["obligation_id"] context = source["autonomous_replan_obligation"]["replan_context"] review = json.loads((project / "review.json").read_text()) writeback = json.loads((project / "writeback.json").read_text()) From ef68665e77df25ff6ef3f343b19f4d354bab0dd2 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:22:53 +0800 Subject: [PATCH 04/33] Finalize release fixture source before its canonical import Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- examples/control_plane/heartbeat-quota-flow-smoke.py | 5 +++-- scripts/qualify-native-goal-release.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/control_plane/heartbeat-quota-flow-smoke.py b/examples/control_plane/heartbeat-quota-flow-smoke.py index 4f2de1834b..6f02efa088 100644 --- a/examples/control_plane/heartbeat-quota-flow-smoke.py +++ b/examples/control_plane/heartbeat-quota-flow-smoke.py @@ -99,7 +99,7 @@ def run_cli_result(root: Path, *args: str, registry_path: Path, runtime: Path) - return result.returncode, json.loads(result.stdout) -def write_fixture(root: Path) -> tuple[Path, Path, Path]: +def write_fixture(root: Path, *, initialize_authority: bool = True) -> tuple[Path, Path, Path]: project = root / "project" runtime = root / "runtime" state_file = f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" @@ -166,7 +166,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: + "\n", encoding="utf-8", ) - import_fixture_authority(registry_path, GOAL_ID) + if initialize_authority: + import_fixture_authority(registry_path, GOAL_ID) return project, runtime, registry_path diff --git a/scripts/qualify-native-goal-release.py b/scripts/qualify-native-goal-release.py index c1b65664bd..6ea8d221e6 100644 --- a/scripts/qualify-native-goal-release.py +++ b/scripts/qualify-native-goal-release.py @@ -112,7 +112,10 @@ def setup( ) fixture = importlib.util.module_from_spec(spec) spec.loader.exec_module(fixture) - project, runtime, registry = fixture.write_fixture(root) + # Finish the release-specific source before the one canonical cold import. + # Importing the generic fixture first fences that source and makes the + # subsequent provider selection reject the already-promoted Goal. + project, runtime, registry = fixture.write_fixture(root, initialize_authority=False) config = json.loads(registry.read_text()) goal = config["goals"][0] goal.update(state_file="ACTIVE_GOAL_STATE.md", coordination={"registered_agents": [AGENT]}) From bcfc7e736ce82f2ab4cbaa8de81dbf7f5848107c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:39:47 +0800 Subject: [PATCH 05/33] Deliver complete captured contracts to structured Turn hosts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/turn_driver/codex_cli.py | 32 ++----- .../turn_driver/host_candidate.py | 85 +++++++++++++++++-- .../test_complete_turn_decision.py | 59 +++++++++++++ .../test_reward_memory_host_isolation.py | 18 ++-- 4 files changed, 157 insertions(+), 37 deletions(-) diff --git a/loopx/control_plane/turn_driver/codex_cli.py b/loopx/control_plane/turn_driver/codex_cli.py index 329630cf30..9f3067fdd9 100644 --- a/loopx/control_plane/turn_driver/codex_cli.py +++ b/loopx/control_plane/turn_driver/codex_cli.py @@ -309,30 +309,12 @@ def codex_cli_result_schema( def _prompt(request: Mapping[str, Any]) -> str: envelope = _mapping(request.get("turn_envelope")) staged = _mapping(_mapping(envelope.get("detail_ref")).get("replan_action_packet")).get("review") - model_request = request - complete = request.get("work_contract") - if complete is not None: - from ..quota.turn_envelope import _turn_envelope_result + from .host_candidate import complete_host_input - _turn_envelope_result("validate_complete", payload=complete, envelope=envelope) - model_request = {key: value for key, value in request.items() if key != "turn_envelope"} - # Capture routes are host transport metadata, not another decision view. - # Keep the complete captured contract immutable; the internal historical - # adapter supplies only the original, hash-bound material readers. - detail = _mapping(envelope.get("detail_ref")) - captured = _mapping(detail.get("captured_decision")) - if captured: - model_request["source_decision_hash"] = _mapping(envelope.get("action_signature"))["source_decision_hash"] - model_request["detail_ref"] = { - "full_decision": detail["full_decision"], - "captured_decision": {**captured, "source_hash_ref": "$.source_decision_hash"}, - } - if staged: - model_request["detail_ref"]["replan_action_packet"] = { - key: value for key, value in _mapping(detail.get("replan_action_packet")).items() - if key in {"review", "writeback"} - } - if staged and complete is None: + complete_input = complete_host_input(request) + complete = complete_input is not None + model_request: Mapping[str, Any] = complete_input if complete_input is not None else request + if staged and not complete: from .host_candidate import extract_turn_authority, replan_prompt_instruction authority = extract_turn_authority(request) @@ -345,7 +327,7 @@ def _prompt(request: Mapping[str, Any]) -> str: ) instructions = [ "Execute exactly one bounded LoopX Turn in the current workspace.", - ("Use work_contract as the complete source of truth. Its interaction_contract.agent_channel carries work_context, required_reads and primary_action; its cli_channel describes Core effects owned by the outer adapter. " if complete is not None else ("Use turn_authority as the verified authority for this Turn. " if staged else "Use the TurnEnvelope as the source of truth. ")) + ("Use work_contract as the complete source of truth. Its interaction_contract.agent_channel carries work_context, required_reads and primary_action; its cli_channel describes Core effects owned by the outer adapter. " if complete else ("Use turn_authority as the verified authority for this Turn. " if staged else "Use the TurnEnvelope as the source of truth. ")) + "Read work_context and remaining required_reads before primary_action; do not repeat fulfilled reads; perform work only when its contract allows it. The selected Todo summary is not its full requirements, and completing that work does not prove Goal completion.", "Do not write LoopX state, spend quota, or apply scheduler changes; the adapter owns those effects.", "Return only the schema-constrained result. For validated_progress, repair_required, or replan_required, fill every material field with public-safe evidence.", @@ -356,7 +338,7 @@ def _prompt(request: Mapping[str, Any]) -> str: "For user_action_required, wait, or iteration_failed, leave material-only fields empty and explain the stop in summary. iteration_failed ends only this iteration and never requests a retry or successor.", 'completed_phases must be exactly ["host_execute","typed_result"], and turn_key must match the request.', ] - if staged and complete is None: + if staged and not complete: instructions.append(replan_prompt_instruction(authority)) elif staged: from .host_candidate import replan_prompt_instruction diff --git a/loopx/control_plane/turn_driver/host_candidate.py b/loopx/control_plane/turn_driver/host_candidate.py index 3b544da5b7..704bfa5443 100644 --- a/loopx/control_plane/turn_driver/host_candidate.py +++ b/loopx/control_plane/turn_driver/host_candidate.py @@ -70,6 +70,36 @@ def _execution_profile_present(request: Mapping[str, Any]) -> bool: return isinstance(profile, Mapping) and bool(profile) +def complete_host_input(request: Mapping[str, Any]) -> dict[str, Any] | None: + """Transport the complete contract plus original material-reader metadata. + + The TypeScript owner validates identity and integrity. This adapter never + compacts decision facts or derives a second model-facing authority view. + Historical requests without a complete contract retain their old decoder. + """ + if "work_contract" not in request: + return None + from ..quota.turn_envelope import _turn_envelope_result + + envelope = _mapping(request.get("turn_envelope")) + _turn_envelope_result("validate_complete", payload=request["work_contract"], envelope=envelope) + model_input = {key: value for key, value in request.items() if key != "turn_envelope"} + detail = _mapping(envelope.get("detail_ref")) + captured = _mapping(detail.get("captured_decision")) + if captured: + model_input["source_decision_hash"] = _mapping(envelope.get("action_signature"))["source_decision_hash"] + model_input["detail_ref"] = { + "full_decision": detail["full_decision"], + "captured_decision": {**captured, "source_hash_ref": "$.source_decision_hash"}, + } + readers = _mapping(detail.get("replan_action_packet")) + if readers.get("review"): + model_input["detail_ref"]["replan_action_packet"] = { + key: value for key, value in readers.items() if key in {"review", "writeback"} + } + return model_input + + def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: """Return the signed action and safety boundary exactly as projected.""" @@ -88,6 +118,11 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: ): raise ValueError("TurnEnvelope action signature is missing or does not match") + try: + model_input = complete_host_input(request) + except RuntimeError as exc: + raise ValueError("complete Turn contract rejected") from exc + action = _mapping(envelope.get("action")) detail = _mapping(envelope.get("detail_ref")) staged = _mapping(detail.get("replan_action_packet")).get("review") or _mapping(envelope.get("replan_action_packet")).get("review") @@ -144,9 +179,9 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: # priorities. Keep the signed envelope intact for the executor; do not send # a second copy ahead of the mandatory immutable review read. for field in (() if staged else ("core_goal", "planning_horizon", "acceptance_index")): - value = detail.get(field, action.get(field)) - if value is not None: - authority[field] = value + detail_value = detail.get(field, action.get(field)) + if detail_value is not None: + authority[field] = detail_value horizon = _mapping(authority.get("planning_horizon")) if horizon: # Relocate only the envelope's registered aliases after flattening its @@ -169,6 +204,10 @@ def extract_turn_authority(request: Mapping[str, Any]) -> dict[str, Any]: if key not in {"replan_action_packet", "replan_read_argv", "core_goal", "planning_horizon", "acceptance_index"}} authority["action_signature"] = signature authority.update(host_context_projection(request)) + if model_input is not None: + # Adapter-local transport only. Result qualification still uses the + # verified internal effect fields; rendering sends the complete input. + authority["_model_input"] = model_input return authority @@ -214,8 +253,9 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals adapter reads back as a typed candidate; it stays public-safe and bounded. """ + model_input = authority.get("_model_input") authority_json = json.dumps( - dict(authority), + model_input if model_input is not None else dict(authority), ensure_ascii=False, sort_keys=True, separators=(",", ":"), @@ -257,7 +297,7 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals "Turn executor. The profile grants no new permissions.\n" if authority.get("execution_profile") else "" ) - return ( + legacy_input = ( "You are executing one bounded LoopX-governed work segment.\n" "The JSON below is the complete host authority for this Turn. Execute " "primary_action only after reading work_context and any remaining required_reads. " @@ -271,7 +311,40 @@ def render_prompt(authority: Mapping[str, Any], *, allow_completion: bool = Fals f"{acceptance_instruction}" f"{work_instruction}" f"Turn authority JSON:\n{authority_json}\n\n" - "When finished, return only one JSON object (no Markdown fence) with " + ) + if model_input is not None: + detail = _mapping(model_input.get("detail_ref")) + input_prompt = ( + "You are executing one bounded LoopX-governed work segment.\n" + "Use work_contract as the complete source of truth. Read its " + "interaction_contract.agent_channel.work_context and remaining required_reads " + "before primary_action; do not repeat fulfilled reads. Perform work only " + "when its contract allows it. Obey every supplied write scope, workspace " + "restriction, approval boundary and stop condition. Checkpointed approval " + "covers only active_write_scope; other approval gates remain.\n" + "Read agent_context before planning; unavailable_context is a failed read, " + "not empty context: discard affected cached context and hold dependent work " + "until recovery. Independent work retains its existing authority.\n" + "Before Goal closeout, preserve vision_continuation_audit's ordered " + "required_before_closeout, evidence requirements and vision_gap_judge. " + "The selected Todo summary is not its full requirements, and Todo " + "completion cannot close the Goal. Use execution_profile to size and " + "qualify work, including artifact, validation, outcome and degradation rules.\n" + "For required additional detail, use detail_ref.full_decision from the " + "saved original observation identified by detail_ref.captured_decision; " + "verify Goal/Agent/Turn and source_decision_hash. Never re-enter admission " + "for detail reads. Missing or invalid evidence holds dependent planning " + "and closure; independent work retains existing authority. Guidance, " + "evidence and reads grant no permissions. Do not write LoopX state, spend " + "quota or apply scheduler changes; the outer Turn executor owns those effects.\n" + + replan_prompt_instruction(detail, packet_path="detail_ref.replan_action_packet") + + "Turn request:\n" + authority_json + "\n\n" + ) + else: + input_prompt = legacy_input + return ( + input_prompt + + "When finished, return only one JSON object (no Markdown fence) with " "these public-safe fields:\n" "- result_kind: one of validated_progress | " + ("validated_completion | " if allow_completion else "") diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 9fb22ae61a..b24df4a210 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -102,10 +102,23 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p actual = json.loads(prompt.split("Turn request:\n", 1)[1]) assert actual["work_contract"] == contract assert "turn_envelope" not in actual and "turn_authority" not in actual + from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt + structured = render_prompt(extract_turn_authority(request)) + structured_input, _ = json.JSONDecoder().raw_decode(structured.split("Turn request:\n", 1)[1]) + assert structured_input == actual + assert "_model_input" not in structured_input tampered = copy.deepcopy(request) tampered["work_contract"]["reason"] = "different facts" with pytest.raises(RuntimeError, match="integrity mismatch"): _prompt(tampered) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + extract_turn_authority(tampered) + from loopx.dsh_goal_mode.turn_host_adapter import DshHostConfig, run_dsh_host + from loopx.control_plane.turn_driver.host_failure import BuiltInHostError + with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): + run_dsh_host(tampered, config=DshHostConfig(workspace=workspace, + dsh_runner=tmp_path / "must-not-load.py")) + assert not (workspace / ".dsh").exists() assert not list(runtime.rglob("decisions/*.json")) assert f._quota_spend_count(runtime) == 0 @@ -241,3 +254,49 @@ def test_native_replan_complete_input_retains_verified_material_readers(tmp_path assert rejected.returncode != 0 captured.write_bytes(original) assert _classification_count(runtime, "quota_slot_spent") == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("prior_todo_binding", [False, True]) +def test_real_structured_host_reads_same_complete_replan_source(tmp_path, monkeypatch, provider, prior_todo_binding): + from tests.control_plane.test_turn_replan_writeback import _fixture, _runner, _run_turn + project, runtime, registry, _state, prior_turn = _fixture(tmp_path, monkeypatch, provider) + runner = _runner(tmp_path, {"result_kind": "wait", "summary": "Original evidence read; no outcome claimed."}) + script = runner.read_text().replace("import json\n", "import json, shlex, subprocess\n") + script = script.replace(" return json.dumps(candidate)", + " candidate.pop('work_evidence', None)\n candidate.pop('validation_evidence', None)\n return json.dumps(candidate)") + script = script.replace(" counter = root / 'host-count.txt'", """ prompt = kwargs["prompt"] + (root / "received.txt").write_text(prompt) + request, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + for stage in ("review", "writeback"): + result = subprocess.run(shlex.split(request["detail_ref"]["replan_action_packet"][stage]), + text=True, capture_output=True, check=True) + (root / (stage + ".json")).write_text(result.stdout) + counter = root / 'host-count.txt'""") + # The runner is a separate Python module; preserve its literal newline escape. + script = script.replace('split("Turn request:\n",', 'split("Turn request:\\n",') + runner.write_text(script) + turn = prior_turn if prior_todo_binding else "structured-complete-replan" + rc, result = _run_turn(project, runtime, registry, runner, turn=turn) + if not prior_todo_binding: + # Structured DSH still has Todo-scoped sessions: an obligation-only + # replan remains blocked rather than inventing a Todo or widening scope. + assert rc == 1 and result["error"] == "LoopX Turn route is not host executable" + assert not (project / "received.txt").exists() + assert _spend_run_count(runtime) == 0 + return + assert rc == 0, json.dumps(result, indent=2) + prompt = (project / "received.txt").read_text() + request, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + source = request["work_contract"] + assert source["schema_version"] == "loopx_turn_envelope_v1" + assert "turn_envelope" not in request and "turn_authority" not in request + saved = list(runtime.rglob("decisions/*.json")) + assert len(saved) == 1 and json.loads(saved[0].read_text()) == source + for stage in ("review", "writeback"): + material = json.loads((project / (stage + ".json")).read_text()) + assert material["ok"] is True + assert material["source_hash"] == request["source_decision_hash"] + assert "before reading detail_ref.replan_action_packet.writeback" in prompt + assert "outer Turn executor owns those effects" in prompt + assert _spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_reward_memory_host_isolation.py b/tests/control_plane/test_reward_memory_host_isolation.py index 5d31e72fff..5edb80be2e 100644 --- a/tests/control_plane/test_reward_memory_host_isolation.py +++ b/tests/control_plane/test_reward_memory_host_isolation.py @@ -394,8 +394,8 @@ def test_real_turn_cli_memory_projection_and_settlement(tmp_path: Path, monkeypa runner = tmp_path / "fixture_dsh_runner.py" runner.write_text('''import json, os, pathlib def run_dsh_turn(*, prompt, **kwargs): - authority = json.loads(prompt.split('Turn authority JSON:\\n', 1)[1].split('\\n\\nWhen finished', 1)[0]) - pathlib.Path(os.environ['FIXTURE_HOST_CAPTURE']).write_text(json.dumps({'prompt': prompt, 'authority': authority})) + request, _ = json.JSONDecoder().raw_decode(prompt.split('Turn request:\\n', 1)[1]) + pathlib.Path(os.environ['FIXTURE_HOST_CAPTURE']).write_text(json.dumps({'prompt': prompt, 'request': request})) return json.dumps({'result_kind': 'validated_progress', 'classification': 'fixture_progress', 'next_action': 'Run the next bounded fixture', 'path_delta_mode': 'unchanged', 'vision_unchanged_reason': 'The original fixture remains open.', @@ -435,17 +435,23 @@ def run_dsh_turn(*, prompt, **kwargs): assert ("Set reward_memory_reflection_json" in observed["prompt"]) is ingest assert ("When reward_memory_recall contains guidance" in observed["prompt"]) is (mode == "recall_context") if mode in {"recall_context", "oversized_recall"}: - memory = observed["authority" if host_kind == "dsh" else "request"]["reward_memory_recall"] + memory = observed["request"]["reward_memory_recall"] assert memory["status"] == "applied" if mode == "recall_context": assert "integration branch" in memory["context"]["guidance"][0]["content_summary"] else: assert memory["context"] is None if mode in {"unconfigured", "disabled", "stale", "both_off"}: - if host_kind == "generic-cli": - assert "reward_memory_recall" not in observed["request"] - else: + request = observed["request"] + assert "reward_memory_recall" not in request + assert "reward_memory_reflection_json" not in request.get("capability_result_fields", {}) + assert not any("reward_memory" in instruction + for instruction in request.get("capability_instructions", [])) + if host_kind != "generic-cli" and "work_contract" not in request: assert "reward_memory" not in observed["prompt"] + # A complete captured source retains inactive configuration/diagnostics. + # Those facts must not activate recall instructions or reflection output; + # deleting them merely to pass a substring check would lose source facts. assert result["effects"]["quota_spent"] is True assert result.get("post_settlement", {}).get("external_writes_performed", False) is False if ingest: From 07ca38f0351d887c31f9524c0be0d034b7e1bb68 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:51:19 +0800 Subject: [PATCH 06/33] Preserve complete Turn contracts in Markdown quota output Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../presentation/renderers/quota_markdown.py | 4 ++ .../renderers/turn_envelope_markdown.py | 4 ++ .../test_complete_turn_decision.py | 37 +++++++++++++++++-- tests/test_turn_envelope.py | 4 +- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/loopx/presentation/renderers/quota_markdown.py b/loopx/presentation/renderers/quota_markdown.py index 81afc46986..97ff029c64 100644 --- a/loopx/presentation/renderers/quota_markdown.py +++ b/loopx/presentation/renderers/quota_markdown.py @@ -351,6 +351,10 @@ def render_quota_should_run_markdown(payload: dict[str, Any]) -> str: from ...control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) + if payload.get("schema_version") == "loopx_turn_envelope_v1": + from .turn_envelope_markdown import render_turn_envelope_markdown + + return render_turn_envelope_markdown(payload) quota = as_dict(payload.get("quota")) lines = [ "# LoopX Quota Should Run", diff --git a/loopx/presentation/renderers/turn_envelope_markdown.py b/loopx/presentation/renderers/turn_envelope_markdown.py index 6edc454532..ca301d10ed 100644 --- a/loopx/presentation/renderers/turn_envelope_markdown.py +++ b/loopx/presentation/renderers/turn_envelope_markdown.py @@ -46,6 +46,10 @@ def render_turn_envelope_markdown(payload: dict[str, Any]) -> str: from ...control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) + if payload.get("schema_version") == "loopx_turn_envelope_v1": + # The complete contract has no separately selected Markdown facts. + # JSON escaping also keeps arbitrary source text inside this one block. + return "# LoopX Turn\n\n```json\n" + json.dumps(payload, ensure_ascii=False, indent=2) + "\n```" action_value = payload.get("action") user_value = payload.get("user") writeback_value = payload.get("writeback") diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index b24df4a210..19ac0d6249 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -1,6 +1,8 @@ """Candidate complete source through actual producers; no model is invoked.""" import copy import json +from pathlib import Path +import subprocess import sys import pytest @@ -36,8 +38,27 @@ def test_complete_source_rejects_tampering_and_binding_substitution(): compose_turn_decision({**source, "schema_version": "unknown"}) +def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): + from loopx.presentation.renderers.quota_markdown import render_quota_should_run_markdown + from loopx.presentation.renderers.turn_envelope_markdown import render_turn_envelope_markdown + + source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, + "interaction_contract": {"agent_channel": {"must_attempt": False, + "required_reads": [{"instruction": "Read the original constraints.\n```\nStop before publication."}]}}, + "future_evidence": {"missing": None, "denied": False, "attempts": 0, "text": "0"}} + contract = compose_turn_decision(source) + before = copy.deepcopy(contract) + for render in (render_quota_should_run_markdown, render_turn_envelope_markdown): + rendered = render(contract) + # The complete JSON remains one source even for a human-readable CLI. + actual = json.loads(rendered.split("```json\n", 1)[1].rsplit("\n```", 1)[0]) + assert actual == before + assert contract == before + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("format_name,envelope_flag", [("json", True), ("markdown", True), ("markdown", False)]) +def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeypatch, provider, format_name, envelope_flag): isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) state = _configure_read_only_todo(project) @@ -47,16 +68,24 @@ def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeyp build_todo_runtime_shadow_projection(goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state)), state_path=state, provider=provider) capture = tmp_path / "capture" - code, output = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, + command = [sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), + "--runtime-root", str(runtime), "--format", format_name, "quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "complete-source", "--codex-app", "--available-capability", "shell", "--available-capability", "filesystem_read", - "--decision-output-dir", str(capture), "--turn-envelope") - assert code == 0, output + "--decision-output-dir", str(capture)] + if envelope_flag: + command.append("--turn-envelope") + process = subprocess.run(command, cwd=Path(__file__).resolve().parents[2], + capture_output=True, text=True, check=False) + assert process.returncode == 0, process.stdout + process.stderr + text = process.stdout if format_name == "json" else process.stdout.split("```json\n", 1)[1].rsplit("\n```", 1)[0] + output = json.loads(text) original = (capture / "decision.json").read_bytes() contract = json.loads(original) assert contract["schema_version"] == "loopx_turn_envelope_v1" assert output["interaction_contract"] == contract["interaction_contract"] assert output["contract_binding"] == contract["contract_binding"] + assert output == contract # Every original fact and instruction survives rendering. assert "action" not in output and "contract_capsule" not in output scope = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id="fixture", context_epoch="one") first = prepare_context_delivery(contract, scope=scope, capture_id="one", surface="complete_turn") diff --git a/tests/test_turn_envelope.py b/tests/test_turn_envelope.py index 99c1f374dc..bf0ae3f3aa 100644 --- a/tests/test_turn_envelope.py +++ b/tests/test_turn_envelope.py @@ -971,10 +971,10 @@ def test_envelope_fallback_preserves_typed_failure_with_or_without_packet(has_pa if has_packet: from loopx.control_plane.effect_runtime import EffectRuntimeRejected with pytest.raises(EffectRuntimeRejected, match="unsupported format: protocol_action_packet"): - _project_quota_cli_payload(failure, Namespace(turn_envelope=True), frozenset(), None) + _project_quota_cli_payload(failure, Namespace(turn_envelope=True, quota_command="should-run"), frozenset(), None) assert failure == before return - rendered = _project_quota_cli_payload(failure, Namespace(turn_envelope=True), frozenset(), None) + rendered = _project_quota_cli_payload(failure, Namespace(turn_envelope=True, quota_command="should-run"), frozenset(), None) assert "interaction_contract must be an object" in rendered.pop("turn_envelope_skipped") assert rendered == before assert failure == before From bd2db817e0b2ff1e8fd9d2b6475fc2b5278cbbc7 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:00:38 +0800 Subject: [PATCH 07/33] Validate complete contracts in captured replan readers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 6 +++ .../test_complete_turn_decision.py | 20 ++++++++ .../control_plane/test_replan_staged_read.py | 48 +++++++++++++++---- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 1d89a5f43e..f2fa30fe7a 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -419,6 +419,12 @@ function replanView(request: JsonObject): JsonObject { if (request.source_hash !== canonicalHash(payload) || payload.ok !== true) { throw new EffectRuntimeRequestError("Replan read requires the original successful decision and matching source hash"); } + // A matching file hash proves which bytes were read, not whether a modern + // contract's version and inner binding are supported. Historical unversioned + // captures keep their decoder; modern captures must pass their shared owner. + if (Object.hasOwn(payload, "schema_version") || Object.hasOwn(payload, "contract_binding")) { + requireCompleteTurnDecision(payload); + } const packet = object(payload.replan_action_packet); const obligation = object(payload.autonomous_replan_obligation); const context = object(obligation.replan_context); diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 19ac0d6249..8b63a5f22e 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -38,6 +38,26 @@ def test_complete_source_rejects_tampering_and_binding_substitution(): compose_turn_decision({**source, "schema_version": "unknown"}) +@pytest.mark.parametrize("mutation", ["version", "content", "binding"]) +def test_replan_reader_refuses_invalid_complete_contract_even_with_matching_file_hash(mutation): + from hashlib import sha256 + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + contract = compose_turn_decision(_quota_payload()) + if mutation == "version": + contract["schema_version"] = "unsupported-future-version" + elif mutation == "content": + contract["replan_action_packet"]["planning_guidance"] = ["Altered obligation"] + else: + contract.pop("contract_binding") + # A caller can truthfully hash the bytes it read without proving that their + # version or inner source binding is valid. Both checks are required. + digest = "sha256:" + sha256(json.dumps(contract, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + with pytest.raises(RuntimeError, match="version|integrity|binding"): + _turn_envelope_result("replan_view", payload=contract, source_hash=digest, stage="review") + + def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): from loopx.presentation.renderers.quota_markdown import render_quota_should_run_markdown from loopx.presentation.renderers.turn_envelope_markdown import render_turn_envelope_markdown diff --git a/tests/control_plane/test_replan_staged_read.py b/tests/control_plane/test_replan_staged_read.py index d6c5cc2078..7d814b8cc2 100644 --- a/tests/control_plane/test_replan_staged_read.py +++ b/tests/control_plane/test_replan_staged_read.py @@ -185,8 +185,8 @@ def assert_explore_entry_and_writer(source, entry, writer): def test_checkpoint_preflight_delivers_the_real_cli_view_and_retains_full_source(tmp_path, monkeypatch, provider, replan, context_delivery): """Exercise the production preflight without a model or evaluator. - A full-source fixture alone missed the original bug: the model must be sent - the captured reader, while checkpoint selection still sees full context. + The production preflight must deliver the complete immutable decision to + both model transport and checkpoint selection without a second admission. """ from benchmark.runtime import checkpoint @@ -231,8 +231,8 @@ def observe(command, **kwargs): assert str(view_path) in body and str(source_path) not in body assert body.endswith("original heartbeat instructions") if context_delivery: - # The optional full/delta transport must present the staged CLI - # view too. Its full recovery must not restore old inline hints. + # Optional full/delta transport must recover this exact complete + # contract, including facts outside the historical action subset. command = body.split("```sh\n", 1)[1].split("\n```", 1)[0] projected = run(shlex.split(command), cwd=fixture["project"], env=env, capture_output=True, text=True, timeout=60) @@ -240,6 +240,9 @@ def observe(command, **kwargs): presentation = json.loads(projected.stdout) assert presentation["ok"] and presentation["mode"] == "full" assert presentation["recovery"]["path"] == str(view_path) + assert json.loads(Path(presentation["path"]).read_bytes()) == source + assert source["schema_version"] == "loopx_turn_envelope_v1" + assert view == source # The checkpoint sends one complete source, not a second fact lens. assert source["heartbeat_receipt"]["turn_instance_id"] == TURN assert source["heartbeat_receipt"] == view["heartbeat_receipt"] assert receipt["checkpoint"]["reason"] == "synthetic_no_native_capture" @@ -255,9 +258,12 @@ def observe(command, **kwargs): assert "replan_action_packet" not in view return assert source["autonomous_replan_obligation"]["replan_context"]["context_id"] - assert "replan_context" not in view["autonomous_replan_obligation"] - packet = view["replan_action_packet"] - assert "planning_guidance" not in packet + assert view["autonomous_replan_obligation"]["replan_context"] == source["autonomous_replan_obligation"]["replan_context"] + assert view["replan_action_packet"]["planning_guidance"] == source["replan_action_packet"]["planning_guidance"] + assert view["replan_action_packet"]["writeback_contract"] == source["replan_action_packet"]["writeback_contract"] + # Historical staged consumers still use the original hash-bound reads. + # This does not qualify their discoverability in the complete CLI input. + packet = build_turn_envelope(source)["replan_action_packet"] capture = Path(source["replan_read_path"]) assert capture.read_bytes() == source_path.read_bytes() before = {key: fixture[key].read_bytes() for key in ("registry", "state", "index")} @@ -268,7 +274,33 @@ def observe(command, **kwargs): assert read["obligation_id"] == selector["obligation_id"] assert read["stage"] == stage if stage == "writeback": - assert_explore_entry_and_writer(source, view, read) + # Complete delivery preserves original capability authoring; + # the old writer still supplies the exact applicable attachment. + original = explore_source(source)["content"]["graph"] + assert explore_source(view)["content"]["graph"] == original + plan = read["writeback"].get("settlement_plan", {}) + steps = plan.get("ordered_steps", []) + attachments = [attachment for step in steps + for attachment in step.get("optional_attachments", []) + if attachment.get("capability_id") == "explore"] + if "settlement_plan" in read["writeback"]: + assert len(attachments) == 1 + assert attachments[0]["attachment_template"] == original["result_attachment_template"] + assert attachments[0]["guidance"] == original["result_writeback_guidance"] + # Correct outer bytes cannot rescue unsupported inner version semantics. + malformed = copy.deepcopy(source) + malformed["schema_version"] = "unsupported-future-version" + from hashlib import sha256 + bad_path = tmp_path / "unsupported.json" + bad_path.write_text(json.dumps(malformed)) + digest = "sha256:" + sha256(json.dumps(malformed, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + command = shlex.split(packet["review"]) + command[command.index("--decision-file") + 1] = str(bad_path) + command[command.index("--source-hash") + 1] = digest + refused = run(command, cwd=fixture["project"], env=env, capture_output=True, text=True, timeout=60) + assert refused.returncode != 0 + assert "unsupported complete Turn decision version" in refused.stdout + refused.stderr assert {key: fixture[key].read_bytes() for key in before} == before assert capture.read_bytes() == source_path.read_bytes() assert len(commands) == 1 # Reads did not re-enter checkpoint preflight. From 66878872bbb6967740be59928f215057360af479 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:11:28 +0800 Subject: [PATCH 08/33] Compose original Turn successor instructions before capture Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 63 ++++++++--- .../test_complete_turn_decision.py | 102 ++++++++++++++++++ 2 files changed, 149 insertions(+), 16 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index f2fa30fe7a..428eb2e243 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -414,6 +414,25 @@ function replanWorkContext(payload: JsonObject, context: JsonObject): JsonObject })}; } +/** Bind successor authoring to the already-decided Turn. This formatting + * owner is shared by complete composition and historical staged reads; it does + * not select work, create a successor or change the settlement plan. + */ +function bindReplanSuccessorContract(value: JsonObject, plan: JsonObject): JsonObject { + const contract = {...value}; + const identity = object(plan.identity); + if (typeof identity.turn_instance_id !== "string") return contract; + const goalRef = object(plan.goal_ref); + contract.successor_binding_args = `--turn-instance-id ${shellQuote(identity.turn_instance_id)}` + + (typeof goalRef.goal_instance_id === "string" ? ` --goal-instance-id ${shellQuote(goalRef.goal_instance_id)}` : ""); + contract.successor_settlement = "If creating a replan successor, include successor_binding_args with its obligation id, then follow the creation receipt's original Turn settlement plan before ending; do not run the successor."; + delete contract.successor_host_action; + if (typeof contract.successor_command === "string") { + contract.successor_command = `${contract.successor_command} ${contract.successor_binding_args}`; + } + return contract; +} + function replanView(request: JsonObject): JsonObject { const payload = requireJsonObject(request.payload, "captured replan decision"); if (request.source_hash !== canonicalHash(payload) || payload.ok !== true) { @@ -459,23 +478,16 @@ function replanView(request: JsonObject): JsonObject { } const writeback = actionProjection(payload, false).writeback; const plan = object(object(writeback).settlement_plan); - const identity = object(plan.identity); - if (typeof identity.turn_instance_id === "string") { - const goalRef = object(plan.goal_ref); - contract.successor_binding_args = `--turn-instance-id ${shellQuote(identity.turn_instance_id)}` + - (typeof goalRef.goal_instance_id === "string" ? ` --goal-instance-id ${shellQuote(goalRef.goal_instance_id)}` : ""); - contract.successor_settlement = "If creating a replan successor, include successor_binding_args with its obligation id, then follow the creation receipt's original Turn settlement plan before ending; do not run the successor."; - delete contract.successor_host_action; - if (typeof contract.successor_command === "string") { - contract.successor_command = `${contract.successor_command} ${contract.successor_binding_args}`; - // Creation returns the original settlement plan directly. A guard - // reentry here would repeat admission/context and can select new work. - object(writeback).next_cli_actions = [contract.successor_command]; - } + const boundContract = payload.schema_version === COMPLETE_TURN_DECISION_VERSION + ? contract : bindReplanSuccessorContract(contract, plan); + if (typeof boundContract.successor_command === "string" && typeof object(plan.identity).turn_instance_id === "string") { + // Creation returns the original settlement plan directly. A guard + // reentry would repeat admission/context and can select new work. + object(writeback).next_cli_actions = [boundContract.successor_command]; } const steps = Array.isArray(plan.ordered_steps) ? plan.ordered_steps.map(object) : []; const refresh = steps.find(step => step.kind === "durable_writeback"); - return {...common, ...sourceRead, writeback_contract: contract, + return {...common, ...sourceRead, writeback_contract: boundContract, required_outcome: packet.required_outcome, allowed_terminal: packet.allowed_terminal, ...(packet.stride_control ? {stride_control: stride} : {}), writeback, @@ -1203,9 +1215,28 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { } assertSupportedQuotaPacket(payload); requireJsonObject(payload.interaction_contract, "Turn interaction contract"); - const result: JsonObject = {...payload, schema_version: COMPLETE_TURN_DECISION_VERSION, + // A complete input must carry the original-Turn successor instructions that + // used to exist only in the staged writer. Compose them before hashing and + // capture; a later material read must not add a competing execution contract. + const packet = object(payload.replan_action_packet); + const interaction = object(payload.interaction_contract); + const cli = object(interaction.cli_channel); + const plan = object(cli.settlement_plan); + let source = payload; + if (packet.obligation_id && packet.settlement_only !== true && + payload.effective_action !== EffectiveAction.GOVERNED_CAPABILITY_INTENT && + packet.obligation_id === object(object(payload.autonomous_replan_obligation).replan_context).obligation_id && + typeof object(plan.identity).turn_instance_id === "string") { + const contract = bindReplanSuccessorContract(object(packet.writeback_contract), plan); + source = {...payload, replan_action_packet: {...packet, writeback_contract: contract}, + ...(typeof contract.successor_command === "string" ? { + interaction_contract: {...interaction, cli_channel: {...cli, + next_cli_actions: [contract.successor_command]}}, + } : {})}; + } + const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, - source_decision_hash: canonicalHash(payload)}}; + source_decision_hash: canonicalHash(source)}}; result.contract_binding = {...object(result.contract_binding), content_hash: canonicalHash(result)}; return result; } diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 8b63a5f22e..9f81bc6875 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -58,6 +58,56 @@ def test_replan_reader_refuses_invalid_complete_contract_even_with_matching_file _turn_envelope_result("replan_view", payload=contract, source_hash=digest, stage="review") +@pytest.mark.parametrize("successor", [False, True]) +def test_complete_replan_carries_original_turn_successor_binding_before_capture(successor): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + plan = {"identity": {"turn_instance_id": "original-turn"}, + "goal_ref": {"goal_instance_id": "original-goal-instance"}} + source["interaction_contract"]["cli_channel"]["settlement_plan"] = plan + if successor: + source["replan_action_packet"]["writeback_contract"] = { + "successor_command": "loopx todo add --goal-id fixture", + "successor_host_action": "end_current_heartbeat"} + before = copy.deepcopy(source) + contract = compose_turn_decision(source) + writer = contract["replan_action_packet"]["writeback_contract"] + assert writer["successor_binding_args"] == ( + "--turn-instance-id original-turn --goal-instance-id original-goal-instance") + assert "original Turn settlement plan before ending" in writer["successor_settlement"] + assert "do not run the successor" in writer["successor_settlement"] + assert "successor_host_action" not in writer + assert contract["interaction_contract"]["cli_channel"]["settlement_plan"] == plan + if successor: + assert writer["successor_command"].endswith(writer["successor_binding_args"]) + assert contract["interaction_contract"]["cli_channel"]["next_cli_actions"] == [writer["successor_command"]] + assert source == before + assert compose_turn_decision(contract) == contract + + +@pytest.mark.parametrize("inactive", ["settlement_only", "capability_intent", "different_context", "unbound"]) +def test_complete_composition_preserves_inactive_replan_authoring(inactive): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}} + source["replan_action_packet"]["writeback_contract"] = { + "successor_command": "loopx todo add --goal-id fixture", + "successor_host_action": "end_current_heartbeat"} + if inactive == "settlement_only": + source["replan_action_packet"]["settlement_only"] = True + elif inactive == "capability_intent": + source["effective_action"] = "governed_capability_intent" + elif inactive == "different_context": + source["autonomous_replan_obligation"]["replan_context"]["obligation_id"] = "different" + else: + source["interaction_contract"]["cli_channel"].pop("settlement_plan") + contract = compose_turn_decision(source) + assert {k: v for k, v in contract.items() if k not in {"schema_version", "contract_binding"}} == source + + def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): from loopx.presentation.renderers.quota_markdown import render_quota_should_run_markdown from loopx.presentation.renderers.turn_envelope_markdown import render_turn_envelope_markdown @@ -76,6 +126,58 @@ def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): assert contract == before +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("host_args", [("--codex-app",), ("--runtime-profile", "generic_cli")]) +def test_complete_quota_successor_settles_original_turn_without_staged_reader(tmp_path, monkeypatch, provider, host_args): + import shlex + from tests.control_plane.test_replan_successor_guard_reentry import _fixture, GOAL, AGENT, TURN + + call, runtime, index = _fixture(tmp_path, monkeypatch, provider, future_monitor=False) + capture = tmp_path / "capture" + guard = call("quota", "should-run", *host_args, "--goal-id", GOAL, + "--agent-id", AGENT, "--turn-instance-id", TURN, "--decision-output-dir", str(capture)) + original = (capture / "decision.json").read_bytes() + assert json.loads(original) == guard + identity = guard["heartbeat_receipt"]["settlement_identity"] + writer = guard["replan_action_packet"]["writeback_contract"] + binding = shlex.split(writer["successor_binding_args"]) + assert binding[binding.index("--turn-instance-id") + 1] == TURN + added = call("todo", "add", "--goal-id", GOAL, "--role", "agent", "--claimed-by", AGENT, + "--text", "Independently verify the source artifact", "--task-class", "advancement_task", + "--action-kind", "validate", "--target-key", "independent-source-artifact", + "--operation-id", "complete-source-successor", "--replan-obligation-id", identity["replan_obligation_id"], + *binding) + assert added["settlement_identity"] == identity + assert added["replan_transition"]["turn_boundary"] == "settle_original_turn_before_end" + steps = added["settlement_plan"]["ordered_steps"] + refresh = next(row["command_template"] for row in steps if row["kind"] == "durable_writeback") + spend = next(row["command_template"] for row in steps if row["kind"] == "quota_spend") + refused = call(*shlex.split(spend)[1:], expected_code=1) + assert refused.get("appended") is not True + for placeholder, value in {"": "successor_created", + "": "single_surface", "": "outcome_progress", + "": "in_flight_continuation"}.items(): + refresh = refresh.replace(placeholder, value) + refreshed = call(*shlex.split(refresh)[1:], "--no-global-sync", "--suppress-external-sinks", + "--vision-unchanged-reason", "The accepted source outcome is unchanged; the successor supplies its next validation.") + assert refreshed["settlement_progress"]["state"] == "spend_required" + spent = call(*shlex.split(spend)[1:]) + assert spent["settlement_progress"]["state"] == "settled" + assert call(*shlex.split(spend)[1:])["appended"] is False + assert sum(json.loads(line).get("classification") == "quota_slot_spent" for line in index.read_text().splitlines()) == 1 + todo = call("todo", "list", "--goal-id", GOAL, "--todo-id", added["todo_id"])["todo"] + assert todo["status"] == "open" # Creating/settling is not executing the successor. + assert (capture / "decision.json").read_bytes() == original + + # After inline execution/settlement, check historical material-reader parity. + from hashlib import sha256 + digest = "sha256:" + sha256(json.dumps(guard, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + staged = _turn_envelope_result("replan_view", payload=guard, source_hash=digest, stage="writeback") + assert staged["writeback_contract"]["successor_binding_args"] == writer["successor_binding_args"] + assert staged["writeback_contract"]["successor_settlement"] == writer["successor_settlement"] + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("format_name,envelope_flag", [("json", True), ("markdown", True), ("markdown", False)]) def test_real_quota_complete_capture_and_differential_recovery(tmp_path, monkeypatch, provider, format_name, envelope_flag): From 2a662fbd2ef40c8ae2f341b7129301224c4b73b0 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:35:20 +0800 Subject: [PATCH 09/33] Reject unknown complete Turn actions before capture and host input Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 19 +++- .../test_complete_turn_decision.py | 96 +++++++++++++++++++ .../test_selected_work_requirements.py | 47 +++++---- 3 files changed, 140 insertions(+), 22 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 428eb2e243..026a683d99 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1,4 +1,5 @@ -import { EffectiveAction } from "./effective_action.generated.ts"; +import { EFFECTIVE_ACTIONS, EffectiveAction } from "./effective_action.generated.ts"; +import { AGENT_SCOPE_FRONTIER_ACTIONS } from "../agents/agent_scope_frontier.generated.ts"; import { createHash } from "node:crypto"; import { ENVELOPED_SHA256_PATTERN } from "../content_digest.ts"; import {omitUnboundExploreResultWriteback, referenceExploreResultWriteback} from "../capabilities/explore_turn_context.ts"; @@ -1171,13 +1172,22 @@ function coldPath( */ export const COMPLETE_TURN_DECISION_VERSION = "loopx_turn_envelope_v1"; +function requireCompleteDecisionSource(source: JsonObject): void { + assertSupportedQuotaPacket(source); + requireJsonObject(source.interaction_contract, "Turn interaction contract"); + // This root field selects execution, unlike optional evidence carried intact + // in the source. Reuse both registered owners; do not inherit the historical + // adapter's unknown-action fallback at the new version boundary. + requireStringLiteral(source.effective_action, + [...EFFECTIVE_ACTIONS, ...AGENT_SCOPE_FRONTIER_ACTIONS], "Turn effective_action"); +} + export function requireCompleteTurnDecision(value: unknown): JsonObject { const decision = requireJsonObject(value, "complete Turn decision"); if (decision.schema_version !== COMPLETE_TURN_DECISION_VERSION) { throw new EffectRuntimeRequestError("unsupported complete Turn decision version"); } const binding = requireJsonObject(decision.contract_binding, "Turn contract binding"); - requireJsonObject(decision.interaction_contract, "Turn interaction contract"); if (typeof binding.scheduler_execution_args !== "string") { throw new EffectRuntimeRequestError("Turn scheduler execution binding must be a string"); } @@ -1193,7 +1203,7 @@ export function requireCompleteTurnDecision(value: unknown): JsonObject { if (binding.source_decision_hash !== canonicalHash(source)) { throw new EffectRuntimeRequestError("complete Turn decision source integrity mismatch"); } - assertSupportedQuotaPacket(source); + requireCompleteDecisionSource(source); return decision; } @@ -1213,8 +1223,7 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { if (Object.hasOwn(payload, "schema_version") || Object.hasOwn(payload, "contract_binding")) { throw new EffectRuntimeRequestError("complete Turn decision source contains reserved version/binding fields"); } - assertSupportedQuotaPacket(payload); - requireJsonObject(payload.interaction_contract, "Turn interaction contract"); + requireCompleteDecisionSource(payload); // A complete input must carry the original-Turn successor instructions that // used to exist only in the staged writer. Compose them before hashing and // capture; a later material read must not add a competing execution contract. diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 9f81bc6875..215e69bd44 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -1,5 +1,6 @@ """Candidate complete source through actual producers; no model is invoked.""" import copy +from hashlib import sha256 import json from pathlib import Path import subprocess @@ -20,8 +21,25 @@ from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime +def _canonical_digest(value): + return "sha256:" + sha256(json.dumps(value, sort_keys=True, + ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() + + +def _future_producer_contract(contract): + contract = copy.deepcopy(contract) + contract["effective_action"] = "future_execution" + source = {key: value for key, value in contract.items() + if key not in {"schema_version", "contract_binding"}} + contract["contract_binding"].pop("content_hash") + contract["contract_binding"]["source_decision_hash"] = _canonical_digest(source) + contract["contract_binding"]["content_hash"] = _canonical_digest(contract) + return contract, source + + def test_complete_source_rejects_tampering_and_binding_substitution(): source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, + "effective_action": "normal_run", "interaction_contract": {}, "future_evidence": {"original": "unchanged"}} before = copy.deepcopy(source) result = compose_turn_decision(source) @@ -38,6 +56,50 @@ def test_complete_source_rejects_tampering_and_binding_substitution(): compose_turn_decision({**source, "schema_version": "unknown"}) +@pytest.mark.parametrize("action", ["future_execution", "", None, 0]) +def test_complete_source_refuses_unknown_required_action_before_capture(action): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + source["effective_action"] = action + original = copy.deepcopy(source) + with pytest.raises(RuntimeError, match="effective_action"): + compose_turn_decision(source) + assert source == original + + +def test_complete_readers_refuse_unknown_action_even_with_valid_integrity(): + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + contract, _ = _future_producer_contract(compose_turn_decision(_quota_payload())) + # A future producer can supply perfectly valid hashes for a semantic value + # this consumer cannot execute. Integrity alone must not admit that action. + with pytest.raises(RuntimeError, match="effective_action"): + compose_turn_decision(contract) + with pytest.raises(RuntimeError, match="effective_action"): + build_turn_envelope(contract) + with pytest.raises(RuntimeError, match="effective_action"): + _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review") + + +def test_complete_action_validation_preserves_registered_union_and_optional_facts(): + from loopx.control_plane.quota.effective_action import EffectiveAction + from loopx.control_plane.agents.agent_scope_frontier import AgentScopeFrontierAction + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + for action in {*EffectiveAction, *AgentScopeFrontierAction}: + source = _quota_payload() + source["effective_action"] = action.value + source["future_evidence"] = {"schema_version": "future_optional_evidence", + "effective_action": "uninterpreted evidence", "result": None} + contract = compose_turn_decision(source) + assert contract["effective_action"] == action.value + assert contract["future_evidence"] == source["future_evidence"] + assert compose_turn_decision(contract) == contract + + @pytest.mark.parametrize("mutation", ["version", "content", "binding"]) def test_replan_reader_refuses_invalid_complete_contract_even_with_matching_file_hash(mutation): from hashlib import sha256 @@ -113,6 +175,7 @@ def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): from loopx.presentation.renderers.turn_envelope_markdown import render_turn_envelope_markdown source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, + "effective_action": "normal_run", "interaction_contract": {"agent_channel": {"must_attempt": False, "required_reads": [{"instruction": "Read the original constraints.\n```\nStop before publication."}]}}, "future_evidence": {"missing": None, "denied": False, "attempts": 0, "text": "0"}} @@ -269,6 +332,7 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): run_dsh_host(tampered, config=DshHostConfig(workspace=workspace, dsh_runner=tmp_path / "must-not-load.py")) + assert not (workspace / ".dsh").exists() assert not list(runtime.rglob("decisions/*.json")) assert f._quota_spend_count(runtime) == 0 @@ -304,6 +368,38 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p run_args = f._base_argv(registry=registry, runtime=runtime, workspace=workspace, codex_bin=executable, model=None, timeout_seconds=20, expected_marker=f._marker_value(1), turn_instance_id="complete-execution") + from loopx.cli_commands import turn as native_turn + real_compose = native_turn.compose_turn_decision + def future_action(payload, **kwargs): + return real_compose({**payload, "effective_action": "future_execution"}, **kwargs) + with monkeypatch.context() as patch: + # Fault only the producer's semantic value. Admission, provider, + # composition, CLI refusal and capture/host boundaries remain real. + patch.setattr(native_turn, "compose_turn_decision", future_action) + code, refused = f._run_cli([*run_args, "--execute"]) + assert code == 1 and "effective_action" in json.dumps(refused), refused + assert not (workspace / "received.txt").exists() + assert not list(runtime.rglob("decisions/*.json")) + assert f._quota_spend_count(runtime) == 0 + # Preserve the historical decoder's existing route, but refuse the same + # unknown execution semantics in a validly hashed complete contract before + # either native host can load a runner or produce a captured execution. + from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan + future_contract, future_source = _future_producer_contract(contract) + legacy_envelope = _turn_envelope_result("build", payload=future_source, + scheduler_execution_args=contract["contract_binding"]["scheduler_execution_args"]) + legacy_plan = build_loopx_turn_plan(legacy_envelope, host="codex-cli", + execution_mode="isolated-headless", turn_instance_id="future-source") + assert legacy_plan["route"]["would_invoke_host"] is True + future_request = build_loopx_turn_host_request({**legacy_plan, "work_contract": future_contract}) + with pytest.raises(RuntimeError, match="effective_action"): + _prompt(future_request) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + extract_turn_authority(future_request) + with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): + run_dsh_host(future_request, config=DshHostConfig(workspace=workspace, + dsh_runner=tmp_path / "must-not-load.py")) + code, executed = f._run_cli([*run_args, "--execute"]) if first_attempt_fails: assert code == 1 and executed["status"] == "failed", executed diff --git a/tests/control_plane/test_selected_work_requirements.py b/tests/control_plane/test_selected_work_requirements.py index b8b817bdeb..7c4c7eb030 100644 --- a/tests/control_plane/test_selected_work_requirements.py +++ b/tests/control_plane/test_selected_work_requirements.py @@ -19,7 +19,6 @@ from loopx.control_plane.todos.active_state_todo_parser import parse_todo_source from loopx.control_plane.todos.goal_todo_projection import retained_todo_summary_fields from loopx.control_plane.turn_driver.codex_cli import _prompt -from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request from loopx.control_plane.turn_driver.host_candidate import extract_turn_authority, render_prompt @@ -111,7 +110,7 @@ def guard(*extra, expected_exit=0): assert packet["scoped_user_gate_fallback"]["selected_executable"]["todo_id"] == "todo_inspect" assert channel["work_context"]["complete"] is True envelope = guard("--turn-envelope") - assert envelope["work_context"]["user_todos"] == channel["work_context"]["user_todos"] + assert envelope["interaction_contract"]["agent_channel"]["work_context"]["user_todos"] == channel["work_context"]["user_todos"] assert state.read_text() == state_text # Canonical tasks still exist: the full Goal source must fail closed, # rather than silently substituting a Todo summary for missing intent. @@ -214,15 +213,12 @@ def guard(*extra: str) -> dict: assert "_context_text_sha256" not in full["selected_todo"] assert reads[-1]["content"]["todo"]["text"] == expected assert json.dumps(context).count(expected) == 1 - assert envelope["required_reads"] == channel["required_reads"] + envelope_channel = envelope["interaction_contract"]["agent_channel"] + assert envelope_channel["required_reads"] == channel["required_reads"] assert context["selected_todo_text_verified"] is True - assert envelope["work_context"] == { - key: value - for key, value in context.items() - if key != "selected_todo_text_verified" - } - assert envelope["action"]["selected_todo"]["text_source_verified"] is True - selected = envelope["action"]["selected_todo"] + assert envelope_channel["work_context"] == context + assert envelope_channel["work_context"]["selected_todo_text_verified"] is True + selected = envelope["selected_todo"] assert selected["todo_id"] == "todo_cursor_work" read = reads[-1] tokens = shlex.split(read["command"]) @@ -242,18 +238,35 @@ def read_detail() -> dict: assert not {"todos", "agent_todos", "user_todos"}.intersection(detail) assert json.dumps(detail).count(expected) == 1 assert detail["todo"]["claimed_by"] == "agent-a" - assert envelope["action_signature"]["matches"] is True assert state.read_bytes() == before - plan = build_loopx_turn_plan(envelope, host="codex-cli", execution_mode="isolated-headless", - turn_instance_id="requirements-synthetic-turn") + def native_plan(turn): + # Use the production native entrypoint and its own host binding rather + # than silently rebinding an App heartbeat capture to a Codex executor. + code, plan = run_json_cli_result("--cli-bin", str(cli_bin), "turn", "plan", + "--goal-id", "requirements-goal", "--agent-id", "agent-a", + "--host", "codex-cli", "--execution-mode", "isolated-headless", + "--turn-instance-id", turn, "--include-transaction-detail", + registry_path=registry, runtime_root=runtime) + assert code == 0 and plan["ok"], plan + assert plan["route"]["would_invoke_host"] is True + assert plan["route"]["host_invocation_allowed"] is False + return plan + + plan = native_plan("requirements-synthetic-turn") request = build_loopx_turn_host_request(plan) assert read["command"] in _prompt(request) assert "work_context" in _prompt(request) + model_input = json.loads(_prompt(request).split("Turn request:\n", 1)[1]) + assert model_input["work_contract"] == plan["work_contract"] + assert "turn_envelope" not in model_input and "turn_authority" not in model_input + assert expected in _prompt(request) authority = extract_turn_authority(request) assert authority["selected_todo"]["todo_id"] == "todo_cursor_work" assert authority["required_reads"] == channel["required_reads"] - assert authority["work_context"] == envelope["work_context"] + assert authority["work_context"] == { + key: value for key, value in context.items() if key != "selected_todo_text_verified" + } assert authority["selected_todo"]["text_source_verified"] is True assert read["command"] in render_prompt(authority) @@ -270,8 +283,8 @@ def read_detail() -> dict: fresh_reads = fresh["interaction_contract"]["agent_channel"]["work_context"]["sources"] assert fresh_reads[-1]["content"]["todo"]["text"] == replacement fresh_envelope = guard("--turn-envelope") - fresh_plan = build_loopx_turn_plan(fresh_envelope, host="codex-cli", execution_mode="isolated-headless", - turn_instance_id="requirements-next-turn") + assert fresh_envelope["interaction_contract"]["agent_channel"]["work_context"]["sources"][-1]["content"]["todo"]["text"] == replacement + fresh_plan = native_plan("requirements-next-turn") fresh_prompt = _prompt(build_loopx_turn_host_request(fresh_plan)) assert replacement in fresh_prompt assert "unknown, not unchanged" in fresh_prompt @@ -441,7 +454,7 @@ def test_short_selected_todo_deduplicates_body_but_keeps_canonical_note( "--agent-id", "agent-a", "--turn-envelope", registry_path=registry, runtime_root=runtime) assert code == 0 and turn["ok"], turn - turn_context = turn["work_context"] + turn_context = turn["interaction_contract"]["agent_channel"]["work_context"] assert turn_context["complete"] is True assert turn_context["selected_todo_ref"] == "selected_todo" turn_source = next(source for source in turn_context["sources"] if source["source"] == "selected_todo") From 451f246bcc903e2277edb9cb255c487eeafefabc Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:49:46 +0800 Subject: [PATCH 10/33] Preserve pure-replan attachment availability in complete contracts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 38 +++++++-- .../test_complete_turn_decision.py | 10 +++ .../control_plane/test_replan_staged_read.py | 84 ++++++++++++++----- 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 026a683d99..823d1b5357 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -381,6 +381,19 @@ function replanGoalContext(payload: JsonObject, reads: JsonObject | null, coreRe return {core, horizon, outcomes, acceptanceIndex}; } +/** Applicability of Explore's existing attachment owner, shared by complete + * composition and the historical staged projection. No authority is granted. */ +function unboundExploreReplan(payload: JsonObject, plan: JsonObject): boolean { + const steps = Array.isArray(plan.ordered_steps) ? plan.ordered_steps.map(object) : []; + const attachments = object(steps.find(step => step.kind === "durable_writeback")).optional_attachments; + const explore = Array.isArray(attachments) ? attachments.map(object).filter(row => row.capability_id === "explore") : []; + const identity = object(plan.identity); + return payload.effective_action === EffectiveAction.AUTONOMOUS_REPLAN_REQUIRED && + selectedWorkTodoId(payload) === null && identity.binding_kind === "autonomous_replan" && + identity.replan_obligation_id === object(payload.replan_action_packet).obligation_id && + !identity.todo_id && explore.length === 0; +} + /** Move only an identical acceptance copy behind the existing captured read. * Different revisions, other source fields and unavailable context stay intact. */ @@ -393,11 +406,7 @@ function replanWorkContext(payload: JsonObject, context: JsonObject): JsonObject const steps = Array.isArray(plan.ordered_steps) ? plan.ordered_steps.map(object) : []; const attachments = object(steps.find(step => step.kind === "durable_writeback")).optional_attachments; const explore = Array.isArray(attachments) ? attachments.map(object).filter(row => row.capability_id === "explore") : []; - const identity = object(plan.identity); - const unboundReplan = payload.effective_action === EffectiveAction.AUTONOMOUS_REPLAN_REQUIRED && - selectedWorkTodoId(payload) === null && identity.binding_kind === "autonomous_replan" && - identity.replan_obligation_id === object(payload.replan_action_packet).obligation_id && - !identity.todo_id && explore.length === 0; + const unboundReplan = unboundExploreReplan(payload, plan); return {...context, sources: context.sources.map(value => { const source = object(value), content = object(source.content); if (source.source === "turn_start_capability_hook" && source.hook_id === "explore.turn_context" && @@ -1243,6 +1252,25 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { next_cli_actions: [contract.successor_command]}}, } : {})}; } + // Retain the existing Explore owner's pure-replan applicability correction + // before capture too. A complete contract must not advertise an attachment + // that requires a Todo when this admission has only an obligation. Reuse the + // exact known-guidance check; custom instructions and evidence stay intact. + // Do not run the historical compactor: supported authoring stays fully inline. + if (unboundExploreReplan(source, plan)) { + const currentInteraction = object(source.interaction_contract); + const agent = object(currentInteraction.agent_channel); + const context = object(agent.work_context); + if (Array.isArray(context.sources)) { + source = {...source, interaction_contract: {...currentInteraction, agent_channel: {...agent, + work_context: {...context, sources: context.sources.map(value => { + const item = object(value); + return item.source === "turn_start_capability_hook" && item.hook_id === "explore.turn_context" && + item.capability_id === "explore" + ? {...item, content: omitUnboundExploreResultWriteback(object(item.content))} : value; + })}}}}; + } + } const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, source_decision_hash: canonicalHash(source)}}; diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 215e69bd44..cc9c076575 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -445,6 +445,9 @@ def test_native_replan_complete_input_retains_verified_material_readers(tmp_path from tests.control_plane.test_turn_replan_writeback import _fixture from tests.control_plane.test_quota_settlement_cli import _classification_count, SELECTED_REPLAN_TODO_ID project, runtime, registry, _state, _prior_turn = _fixture(tmp_path, monkeypatch, provider) + configuration = json.loads(registry.read_text()) + configuration["goals"][0]["explore_graph"] = {"enabled": True} + registry.write_text(json.dumps(configuration)) turn = _prior_turn if prior_todo_binding else "complete-replan-readers" executable = tmp_path / "replan-host" executable.write_text("#!" + sys.executable + ''' @@ -481,6 +484,13 @@ def test_native_replan_complete_input_retains_verified_material_readers(tmp_path else: assert not identity["todo_id"] assert identity["replan_obligation_id"] == source["autonomous_replan_obligation"]["obligation_id"] + graph = next(row["content"]["graph"] for row in + source["interaction_contract"]["agent_channel"]["work_context"]["sources"] + if row.get("hook_id") == "explore.turn_context") + assert ("result_attachment_template" in graph) is prior_todo_binding + assert ("unavailable for this pure replan" in graph["result_writeback_guidance"]) is not prior_todo_binding + assert "Read measurement_feedback before repeating a measured route" in graph["result_writeback_guidance"] + assert graph["record_finding_template"] and graph["record_node_template"] context = source["autonomous_replan_obligation"]["replan_context"] review = json.loads((project / "review.json").read_text()) writeback = json.loads((project / "writeback.json").read_text()) diff --git a/tests/control_plane/test_replan_staged_read.py b/tests/control_plane/test_replan_staged_read.py index 7d814b8cc2..238d19f820 100644 --- a/tests/control_plane/test_replan_staged_read.py +++ b/tests/control_plane/test_replan_staged_read.py @@ -14,7 +14,7 @@ from canonical_authority_fixture import isolate_sqlite_runtime from loopx.control_plane.effect_runtime import EffectRuntimeRejected, restart_effect_runtime from loopx.control_plane.quota.turn_envelope import ( - _turn_envelope_result, build_turn_envelope, quota_action_signature_document, + _turn_envelope_result, build_turn_envelope, compose_turn_decision, quota_action_signature_document, turn_envelope_action_signature_document, prepare_context_delivery, restore_context_delivery, ) from test_replan_host_context_projection import _quota_payload @@ -107,8 +107,11 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, entry = call("quota", "should-run", "--codex-app", "--goal-id", goal, "--agent-id", agent, "--turn-instance-id", turn, "--decision-output-dir", str(capture)) source = json.loads((capture / "decision.json").read_bytes()) - original = explore_source(source)["content"]["graph"] + raw_context = call("explore", "turn-context", "--goal-id", goal, "--agent-id", agent) + original = raw_context["graph"] graph = explore_source(entry)["content"]["graph"] + assert entry == source # Applicability is composed before immutable capture. + assert entry["schema_version"] == "loopx_turn_envelope_v1" assert entry["heartbeat_receipt"]["settlement_identity"]["binding_kind"] == "autonomous_replan" assert not entry.get("selected_todo") assert "result_attachment_template" not in graph and "result_writeback_read" not in graph @@ -120,7 +123,7 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, "record_node_template", "record_finding_template", "guidance"): assert graph.get(key) == original.get(key) assert json_bytes(original) - json_bytes(graph) > 2000 - writer_run = subprocess.run(shlex.split(entry["replan_action_packet"]["writeback"]), + writer_run = subprocess.run(shlex.split(build_turn_envelope(entry)["replan_action_packet"]["writeback"]), capture_output=True, text=True, check=True) writer = json.loads(writer_run.stdout) assert all(not row.get("optional_attachments") @@ -129,7 +132,7 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, # not suggest a new way around it or change its primary commit behavior. attachment = {**original["result_attachment_template"], "question": "Is the source valid?", "applicability": "Synthetic source", "input_revision": "source-v1", "observation": "Reviewed", - "interpretation": "Unresolved", "evidence_refs": ["evidence:source-review"]} + "interpretation": "Unresolved", "status": "tentative", "evidence_refs": ["evidence:source-review"]} with pytest.raises(ValueError, match="requires agent, Todo and Turn identity"): prepare_result_attachment(attachment, registry_path=tmp_path / "registry.json", runtime_root=runtime, goal_id=goal, agent_id=agent, todo_id=None, turn_instance_id=turn) @@ -137,6 +140,9 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, # identity keep the old payload rather than guessing applicability. for mutation in ("capture", "custom", "identity", "attachment"): changed = copy.deepcopy(source) + changed.pop("schema_version") + changed.pop("contract_binding") + explore_source(changed)["content"]["graph"] = copy.deepcopy(original) expected = explore_source(changed)["content"] if mutation == "capture": changed.pop("replan_read_path") @@ -150,6 +156,30 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, step["optional_attachments"] = [{"capability_id": "explore", "guidance": "Custom authoring"}] projected = _turn_envelope_result("replan_cli_view", payload=changed) assert explore_source(projected)["content"] == expected + # Complete composition uses the same conservative owner before capture, + # without depending on a staged-reader path. Unsupported/custom source + # semantics remain inline, rather than being silently normalized away. + for mutation in ("custom", "template", "identity", "attachment", "bound"): + changed = {key: copy.deepcopy(value) for key, value in source.items() + if key not in {"schema_version", "contract_binding"}} + changed.pop("replan_read_path") + expected = explore_source(changed)["content"] + expected["graph"] = copy.deepcopy(original) + if mutation == "custom": + expected["graph"]["result_writeback_guidance"] += " Preserve a custom obligation." + elif mutation == "template": + expected["graph"]["result_attachment_template"]["input_revision"] = "custom-revision" + elif mutation == "identity": + changed["interaction_contract"]["cli_channel"]["settlement_plan"]["identity"].pop("binding_kind") + elif mutation == "bound": + changed["selected_todo"] = {"todo_id": "todo_synthetic_bound"} + else: + step = next(row for row in changed["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] + if row["kind"] == "durable_writeback") + step["optional_attachments"] = [{"capability_id": "explore", "guidance": "Custom authoring"}] + before = copy.deepcopy(changed) + assert explore_source(compose_turn_decision(changed))["content"] == expected + assert changed == before assert json.loads((capture / "decision.json").read_bytes()) == source finally: restart_effect_runtime(wait_for_exit=True) @@ -349,16 +379,21 @@ def test_real_guard_capture_read_and_writeback_without_another_guard(tmp_path, m "--goal-id", GOAL, "--agent-id", AGENT, "--turn-instance-id", TURN, "--todo-id", TODO, "--decision-output-dir", str(capture), *(["--turn-envelope"] if envelope else [])) - packet = guard["replan_action_packet"] - if not envelope: - assert "settlement_plan" not in guard["interaction_contract"]["cli_channel"] - assert guard["interaction_contract"]["cli_channel"]["settlement_read_ref"] == "$.replan_action_packet.writeback" + # Exercise the retained historical material-reader codec against the + # actual new capture. Complete input itself needs no deferred body. + packet = build_turn_envelope(guard)["replan_action_packet"] + assert guard["schema_version"] == "loopx_turn_envelope_v1" + assert "settlement_plan" in guard["interaction_contract"]["cli_channel"] + assert "settlement_read_ref" not in guard["interaction_contract"]["cli_channel"] saved = (capture / "decision.json").read_bytes() source = json.loads(saved) + assert guard == source # Unknown old/custom prose, different templates, ambiguous/missing # writers and missing capture must remain fully inline. for mutation in ("custom", "template", "duplicate", "missing", "capture"): changed = copy.deepcopy(source) + changed.pop("schema_version") + changed.pop("contract_binding") graph = explore_source(changed)["content"]["graph"] step = next(row for row in changed["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] if row["kind"] == "durable_writeback") @@ -376,9 +411,8 @@ def test_real_guard_capture_read_and_writeback_without_another_guard(tmp_path, m changed.pop("replan_read_path") projected = _turn_envelope_result("replan_cli_view", payload=changed) assert explore_source(projected)["content"]["graph"] == graph - entry_writeback = guard["writeback"] if envelope else guard["interaction_contract"]["cli_channel"] - assert "settlement_plan" not in entry_writeback and "next_cli_actions" not in entry_writeback - assert entry_writeback["settlement_identity"] == source["heartbeat_receipt"]["settlement_identity"] + entry_writeback = guard["interaction_contract"]["cli_channel"] + assert entry_writeback["settlement_plan"]["identity"] == source["heartbeat_receipt"]["settlement_identity"] original_files = {key: fixture[key].read_bytes() for key in ("registry", "state", "index")} # Executable commands come from the production packet. Deliberately use # an absent registry: the reader must never collect state or re-admit. @@ -396,25 +430,29 @@ def read(stage, expected_code=0): assert result.returncode == expected_code, (result.stdout, result.stderr) return json.loads(result.stdout) review, writer = read("review"), read("writeback") - if not envelope: - assert_explore_entry_and_writer(source, guard, writer) - else: - assert_explore_entry_and_writer(source, { - "interaction_contract": {"agent_channel": {"work_context": guard["work_context"]}}, - "replan_action_packet": packet}, writer) + # Complete input keeps supported authoring inline. Historical staged + # reads remain exact material reads, not a second execution contract. + assert explore_source(guard)["content"] == explore_source(source)["content"] + original_graph = explore_source(source)["content"]["graph"] + writer_step = next(row for row in writer["writeback"]["settlement_plan"]["ordered_steps"] + if row["kind"] == "durable_writeback") + attachment = next(row for row in writer_step["optional_attachments"] + if row["capability_id"] == "explore") + assert attachment["guidance"] == original_graph["result_writeback_guidance"] + assert attachment["attachment_template"] == original_graph["result_attachment_template"] assert writer["writeback"]["settlement_plan"] == source["interaction_contract"]["cli_channel"]["settlement_plan"] assert writer["settlement_command"].endswith(" --settle-turn") - # Real CLI/default-off and native readers both deliver the original - # planning facts after admission, without a second body in the entry. + # Both retired flag values deliver complete planning facts. The + # original captured material reads cannot replace them with fresh state. expected_goal = source["autonomous_replan_obligation"]["replan_context"]["core_goal"] assert review["core_goal"] == expected_goal assert review["action_portfolio"] == source.get("action_portfolio") - if not envelope: - assert "core_goal" not in guard and "planning_horizon" not in guard - assert guard.get("action_portfolio") == source.get("action_portfolio") + assert guard.get("core_goal") == source.get("core_goal") + assert guard.get("planning_horizon") == source.get("planning_horizon") + assert guard.get("action_portfolio") == source.get("action_portfolio") assert review["context_id"] == writer["context_id"] assert review["obligation_id"] == source["replan_action_packet"]["obligation_id"] - assert {k: v for k, v in writer["writeback_contract"].items() if k not in {"successor_binding_args", "successor_settlement"}} == without_example(source["replan_action_packet"]["writeback_contract"]) + assert writer["writeback_contract"] == without_example(source["replan_action_packet"]["writeback_contract"]) assert (capture / "decision.json").read_bytes() == saved assert {key: fixture[key].read_bytes() for key in original_files} == original_files assert not reader_fixture["registry"].exists() From 834e32f434878f53859c6d2d97c578c995b83733 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 07:05:43 +0800 Subject: [PATCH 11/33] Qualify complete context through native resume and recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 14 +++++++++----- tests/control_plane/test_context_delivery_cli.py | 8 ++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index f6cacd2ae5..08e1cf3a0f 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -640,7 +640,8 @@ def held(*args, **kwargs): projection = Path(wires["warm"]["recovery"]["path"]) assert hashlib.sha256(projection.read_bytes()).hexdigest() == wires["warm"]["recovery"]["sha256"] current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, - capture_id=wires["warm"]["target"]["capture_id"], surface=surface) + capture_id=wires["warm"]["target"]["capture_id"], surface="complete_turn") + assert current["target"] == wires["warm"]["target"] baseline = wires["cold"] if resumed and qualified_resume and not resume_loss: assert wires["warm"]["base"] == baseline["target"] @@ -652,13 +653,16 @@ def held(*args, **kwargs): assert current["target"]["context_epoch"] != baseline["target"]["context_epoch"] baseline = prepare_context_delivery(baseline["context"], scope={ k: current["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, - capture_id=baseline["target"]["capture_id"], surface=surface) + capture_id=baseline["target"]["capture_id"], surface="complete_turn") assert baseline["target"] == wires["warm"]["base"] restored = restore_context_delivery(wires["warm"], baseline=baseline, current=current) assert restored["context"] == current["context"] and restored["mode"] == wires["warm"]["mode"] - for document in (wires["cold"]["context"], current["context"]): - context = (document["work_context"] if surface == "turn_envelope" else - document["interaction_contract"]["agent_channel"]["work_context"]) + # Both CLI spellings now emit the same complete contract. Compare every + # reconstructed fact with its original capture, not two compact views. + for document, (_, raw) in zip((wires["cold"]["context"], current["context"]), snapshots, strict=True): + assert document == json.loads(raw) + assert document["schema_version"] == "loopx_turn_envelope_v1" + context = document["interaction_contract"]["agent_channel"]["work_context"] selected = next(source["content"]["todo"] for source in context["sources"] if source["source"] == "selected_todo") assert selected["todo_id"] == TODO_ID and selected["text"] == body diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 508b30216f..6c27143dcc 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -277,8 +277,12 @@ def release(): assert warm["mode"] == "delta" projection = json.loads((second / "projection.json").read_bytes()) current = prepare_context_delivery(projection, scope=scope, - capture_id=warm["target"]["capture_id"], surface=surface) - assert restore_context_delivery(warm, baseline=cold, current=current)["context"] == projection + capture_id=warm["target"]["capture_id"], surface="complete_turn") + assert projection == json.loads((second / "decision.json").read_bytes()) + assert projection["schema_version"] == "loopx_turn_envelope_v1" + assert current["target"] == warm["target"] + restored = restore_context_delivery(warm, baseline=cold, current=current) + assert restored["mode"] == "delta" and restored["context"] == projection assert len(json.dumps(warm)) < len(json.dumps(current)) assert warm["recovery"]["path"] == str(second / "projection.json") assert warm["target"]["capture_id"] == hashlib.sha256((second / "decision.json").read_bytes()).hexdigest() From d70bf26d212c6f5102c81a3347ff6e3fb8c0aca7 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 07:15:54 +0800 Subject: [PATCH 12/33] Qualify complete DSH context and bound replan inputs Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_turn_host_context_delivery.py | 111 ++++++++++-------- 1 file changed, 62 insertions(+), 49 deletions(-) diff --git a/tests/control_plane/test_turn_host_context_delivery.py b/tests/control_plane/test_turn_host_context_delivery.py index e43ee0eba2..fc4326f939 100644 --- a/tests/control_plane/test_turn_host_context_delivery.py +++ b/tests/control_plane/test_turn_host_context_delivery.py @@ -10,7 +10,9 @@ import pytest from loopx.control_plane.quota.turn_envelope import build_turn_envelope, turn_envelope_action_signature_document +from loopx.control_plane.turn_driver.decision_capture import prepare_turn_decision_capture from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan +from loopx.control_plane.turn_driver.executor import build_loopx_turn_host_request from loopx.control_plane.turn_driver.host_candidate import _canonical_hash from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.test_agent_context import POLICY @@ -47,6 +49,9 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk assert code == 0, envelope original = (capture / "decision.json").read_bytes() source = json.loads(original) + assert envelope == source + assert source["schema_version"] == "loopx_turn_envelope_v1" + envelope = build_turn_envelope(source, captured_decision_path=str(capture / "decision.json")) context = source["interaction_contract"].get("agent_context") assert bool(context) is (mode != "off") if not context: @@ -70,43 +75,49 @@ def test_captured_context_reaches_actual_host_without_readmission(tmp_path, monk ) command = [sys.executable, "-m", "loopx.dsh_goal_mode", "--workspace", str(project), "--dsh-home", str(tmp_path / "host-home"), "--dsh-runner", str(runner)] - request = {"schema_version": "loopx_turn_host_request_v0", "turn_key": "sha256:" + "0" * 64, - "route": "primary_delivery", "turn_envelope": envelope} + plan = build_loopx_turn_plan(envelope, host="dsh", execution_mode="isolated-headless", + turn_instance_id="host-context-turn") + if mode != "legacy_reference": + plan["work_contract"] = source + request = build_loopx_turn_host_request(plan) before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} result = subprocess.run(command, input=json.dumps(request), text=True, capture_output=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert json.loads(result.stdout)["result_kind"] == "wait" prompt_path = project / "received.txt" prompt = prompt_path.read_text() - authority = json.loads(prompt.split("Turn authority JSON:\n", 1)[1].split("\n\nWhen finished", 1)[0]) - if context: - # Independent source oracle after the actual subprocess boundary: two - # agreeing reduced projections cannot prove delivery of the full source. + if mode == "legacy_reference": + # Historical decoder remains explicitly qualified until its reader exits. + authority, _ = json.JSONDecoder().raw_decode(prompt.split("Turn authority JSON:\n", 1)[1]) assert authority["agent_context"] == envelope["agent_context"] - if mode == "legacy_reference": - assert authority["action_signature"] == envelope["action_signature"] - detail = authority["detail_ref"] - assert detail == envelope["detail_ref"] - read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout - assert read == original - # Full decision JSON includes numeric values whose canonical wire - # is owned by TS (e.g. Python 1.0 and JSON.stringify's 1 differ). - assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] - assert _canonical_hash(json.loads(read)["interaction_contract"]["agent_context"]) == authority["agent_context"]["content_hash"] - else: - assert envelope["agent_context"] == context - assert "detail_ref" not in envelope["agent_context"] - assert envelope["compaction"]["within_budget"] is False - assert "detail_ref" not in authority + assert authority["action_signature"] == envelope["action_signature"] + detail = authority["detail_ref"] + read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout + assert read == original + assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] + assert _canonical_hash(json.loads(read)["interaction_contract"]["agent_context"]) == authority["agent_context"]["content_hash"] invalid = deepcopy(request) invalid["turn_envelope"]["agent_context"]["scope"]["agent_id"] = "wrong-agent" rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) assert rejected.returncode != 0 and "action signature" in rejected.stderr - assert prompt_path.read_text() == prompt # Refusal precedes the provider seam. + assert prompt_path.read_text() == prompt else: - assert "agent_context" not in authority - assert "detail_ref" not in authority - assert "action_signature" not in authority + actual, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + # Compare at the real subprocess/provider seam with the independent + # immutable capture, including full acceptance, context and stop facts. + assert actual["work_contract"] == source + assert "turn_envelope" not in actual and "turn_authority" not in actual + delivered_context = actual["work_contract"]["interaction_contract"].get("agent_context") + assert delivered_context == context + if context: + assert "detail_ref" not in delivered_context + for field, value in (("reason", "Changed original facts"), + ("schema_version", "loopx_turn_envelope_v999")): + invalid = deepcopy(request) + invalid["work_contract"][field] = value + rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) + assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr + assert prompt_path.read_text() == prompt # Refusal precedes the provider seam. assert (capture / "decision.json").read_bytes() == original assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == before @@ -136,14 +147,20 @@ def test_bound_replan_reaches_actual_host_with_original_evidence(tmp_path, monke "--todo-id", SELECTED_REPLAN_TODO_ID, "--turn-instance-id", turn, "--codex-app", "--turn-envelope", "--decision-output-dir", str(capture), cwd=project) assert code == 0, envelope - assert envelope["action"]["delivery_allowed"] is False - assert envelope["action"]["must_attempt"] is True + original = (capture / "decision.json").read_bytes() + source = json.loads(original) + assert envelope == source + assert source["schema_version"] == "loopx_turn_envelope_v1" + agent_channel = source["interaction_contract"]["agent_channel"] + assert agent_channel["delivery_allowed"] is False + assert agent_channel["must_attempt"] is True + envelope = build_turn_envelope(source) plan = build_loopx_turn_plan(envelope, host="dsh", execution_mode="isolated-headless", turn_instance_id=turn) + plan = prepare_turn_decision_capture(plan, journal_path=tmp_path / "turn" / "journal.json", + decision=source, existing_journal=False) assert plan["route"]["kind"] == "replan_required" assert plan["route"]["would_invoke_host"] is True - original = (capture / "decision.json").read_bytes() - source = json.loads(original) assert source["heartbeat_receipt"]["settlement_identity"]["todo_id"] == SELECTED_REPLAN_TODO_ID runner = tmp_path / "runner.py" @@ -155,44 +172,40 @@ def test_bound_replan_reaches_actual_host_with_original_evidence(tmp_path, monke ) command = [sys.executable, "-m", "loopx.dsh_goal_mode", "--workspace", str(project), "--dsh-home", str(tmp_path / "host-home"), "--dsh-runner", str(runner)] - request = {"schema_version": "loopx_turn_host_request_v0", "turn_key": plan["transaction"]["turn_key"], - "route": "replan_required", "turn_envelope": envelope} + request = build_loopx_turn_host_request({**plan, "work_contract": source}) before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} result = subprocess.run(command, input=json.dumps(request), text=True, capture_output=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert json.loads(result.stdout)["result_kind"] == "wait" prompt_path = project / "received.txt" prompt = prompt_path.read_text() - authority = json.loads(prompt.split("Turn authority JSON:\n", 1)[1].split("\n\nWhen finished", 1)[0]) - # Compare preserved choices with the complete source and execute the reader - # actually delivered to the provider. No unconditional full-source detour. - packet = authority["replan_action_packet"] - for field in ("obligation_id", "required_outcome", "allowed_terminal"): - assert packet[field] == source["replan_action_packet"][field] + actual, _ = json.JSONDecoder().raw_decode(prompt.split("Turn request:\n", 1)[1]) + assert actual["work_contract"] == source + assert "turn_envelope" not in actual and "turn_authority" not in actual + # Execute the original observation readers actually delivered to the provider. + detail = actual["detail_ref"] + packet = detail["replan_action_packet"] review = subprocess.run(shlex.split(packet["review"]), capture_output=True, text=True, check=True) - assert json.loads(review.stdout)["context_id"] == source["autonomous_replan_obligation"]["replan_context"]["context_id"] - assert authority["action_signature"] == envelope["action_signature"] - detail = authority["detail_ref"] - assert detail == envelope["detail_ref"] + context = source["autonomous_replan_obligation"]["replan_context"] + assert json.loads(review.stdout)["context_id"] == context["context_id"] captured = detail["captured_decision"] assert (captured["goal_id"], captured["agent_id"], captured["turn_instance_id"]) == (GOAL_ID, AGENT_ID, turn) read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout assert read == original - assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] - context = json.loads(read)["autonomous_replan_obligation"]["replan_context"] - assert context["obligation_id"] == authority["replan_action_packet"]["obligation_id"] - goal_read = next(r for r in authority["required_reads"] if r["source"] == "goal_state") + assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == actual["source_decision_hash"] + assert context["obligation_id"] == source["replan_action_packet"]["obligation_id"] + goal_read = next(r for r in agent_channel["required_reads"] if r["source"] == "goal_state") delivered_goal = subprocess.run(shlex.split(goal_read["command"]), capture_output=True, check=True).stdout assert delivered_goal == goal_bytes and stop.encode() in delivered_goal - assert "Read replan_action_packet.review" in prompt - assert "read autonomous_replan_obligation.replan_context" not in prompt + assert "Read detail_ref.replan_action_packet.review" in prompt + assert "before reading detail_ref.replan_action_packet.writeback" in prompt assert "Never re-enter quota admission" in prompt assert "Do not execute writeback commands" in prompt for field in ("obligation_id", "planning_guidance", "uncovered_frontier"): invalid = deepcopy(request) - invalid["turn_envelope"]["replan_action_packet"][field] = "changed after signing" + invalid["work_contract"]["replan_action_packet"][field] = "changed after signing" rejected = subprocess.run(command, input=json.dumps(invalid), text=True, capture_output=True, timeout=30) - assert rejected.returncode != 0 and "action signature" in rejected.stderr + assert rejected.returncode != 0 and "complete Turn contract rejected" in rejected.stderr assert prompt_path.read_text() == prompt assert (capture / "decision.json").read_bytes() == original assert state.read_bytes() == goal_bytes From 895f38fc5c2e1bf9e06b3f9da7f5f2ac6c80f97c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:02:38 +0800 Subject: [PATCH 13/33] fix(turn): validate complete interaction semantics before dispatch Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/turn_envelope.ts | 6 +- .../test_complete_turn_decision.py | 75 +++++++++++++++++-- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 823d1b5357..e17b9b927b 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -17,7 +17,7 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { projectTurnStartUnavailableContext } from "../capability_hooks.ts"; import { requireBoolean, requireJsonObject, requireNonEmptyString, requireStringLiteral } from "../runtime_decode.ts"; import { projectPendingCapabilityIntent } from "../work_items/pending_capability_intent.ts"; -import { projectInteractionRequiredReads, selectedWorkTodoId, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; +import { decodeInteractionContract, projectInteractionRequiredReads, selectedWorkTodoId, USER_CHANNEL_NOTIFICATION_POLICIES } from "../work_items/interaction_contract.ts"; import { measureTurnEnvelope, TURN_ENVELOPE_SECTION_TARGETS } from "./turn_envelope_budget.ts"; export { TURN_ENVELOPE_BUDGET_BYTES } from "./turn_envelope_budget.ts"; @@ -1183,7 +1183,9 @@ export const COMPLETE_TURN_DECISION_VERSION = "loopx_turn_envelope_v1"; function requireCompleteDecisionSource(source: JsonObject): void { assertSupportedQuotaPacket(source); - requireJsonObject(source.interaction_contract, "Turn interaction contract"); + // Validate executable semantics with their owner, retaining the original + // object (including optional facts) rather than its normalized decode. + decodeInteractionContract(source.interaction_contract); // This root field selects execution, unlike optional evidence carried intact // in the source. Reuse both registered owners; do not inherit the historical // adapter's unknown-action fallback at the new version boundary. diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index cc9c076575..c12ba6a52a 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -21,14 +21,18 @@ from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime +def _valid_interaction(): + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + return _quota_payload()["interaction_contract"] + + def _canonical_digest(value): return "sha256:" + sha256(json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest() -def _future_producer_contract(contract): - contract = copy.deepcopy(contract) - contract["effective_action"] = "future_execution" +def _resign_contract(contract): source = {key: value for key, value in contract.items() if key not in {"schema_version", "contract_binding"}} contract["contract_binding"].pop("content_hash") @@ -37,10 +41,54 @@ def _future_producer_contract(contract): return contract, source +def _future_producer_contract(contract): + contract = copy.deepcopy(contract) + contract["effective_action"] = "future_execution" + return _resign_contract(contract) + + +@pytest.mark.parametrize("mutation, error", [ + ("version", "schema_version"), + ("boolean", "must_attempt"), + ("notification", "notify"), + ("contradiction", "quiet"), +]) +def test_complete_readers_reject_invalid_interaction_with_valid_integrity(mutation, error): + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from tests.control_plane.test_replan_host_context_projection import _quota_payload + + source = _quota_payload() + interaction = source["interaction_contract"] + if mutation == "version": + interaction["schema_version"] = "future_interaction_version" + elif mutation == "boolean": + interaction["agent_channel"]["must_attempt"] = "true" + elif mutation == "notification": + interaction["user_channel"]["notify"] = "FUTURE_NOTIFY" + else: + interaction["agent_channel"]["quiet_noop_allowed"] = True + # Obtain a valid current contract, then emulate a producer with different + # required semantics and correct integrity hashes. Every reader must refuse. + contract = compose_turn_decision(_quota_payload()) + contract["interaction_contract"] = interaction + contract, _ = _resign_contract(contract) + before = copy.deepcopy(source) + for read in ( + lambda: compose_turn_decision(source), + lambda: compose_turn_decision(contract), + lambda: build_turn_envelope(contract), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review"), + ): + with pytest.raises(RuntimeError, match=error): + read() + assert source == before + + def test_complete_source_rejects_tampering_and_binding_substitution(): source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, "effective_action": "normal_run", - "interaction_contract": {}, "future_evidence": {"original": "unchanged"}} + "interaction_contract": {**_valid_interaction(), "future_optional": {"value": None}}, "future_evidence": {"original": "unchanged"}} before = copy.deepcopy(source) result = compose_turn_decision(source) assert {k: v for k, v in result.items() if k not in {"schema_version", "contract_binding"}} == before @@ -176,9 +224,10 @@ def test_complete_markdown_keeps_unknown_facts_types_and_instruction_text(): source = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}, "effective_action": "normal_run", - "interaction_contract": {"agent_channel": {"must_attempt": False, - "required_reads": [{"instruction": "Read the original constraints.\n```\nStop before publication."}]}}, + "interaction_contract": _valid_interaction(), "future_evidence": {"missing": None, "denied": False, "attempts": 0, "text": "0"}} + source["interaction_contract"]["agent_channel"]["required_reads"] = [ + {"instruction": "Read the original constraints.\n```\nStop before publication."}] contract = compose_turn_decision(source) before = copy.deepcopy(contract) for render in (render_quota_should_run_markdown, render_turn_envelope_markdown): @@ -400,6 +449,20 @@ def future_action(payload, **kwargs): run_dsh_host(future_request, config=DshHostConfig(workspace=workspace, dsh_runner=tmp_path / "must-not-load.py")) + invalid_interaction = copy.deepcopy(request) + invalid_interaction["work_contract"]["interaction_contract"]["schema_version"] = "future_interaction_version" + invalid_interaction["work_contract"], _ = _resign_contract(invalid_interaction["work_contract"]) + with pytest.raises(RuntimeError, match="interaction_contract.schema_version"): + _prompt(invalid_interaction) + with pytest.raises(ValueError, match="complete Turn contract rejected"): + extract_turn_authority(invalid_interaction) + with pytest.raises(BuiltInHostError, match="dsh_turn_authority_rejected"): + run_dsh_host(invalid_interaction, config=DshHostConfig(workspace=workspace, + dsh_runner=tmp_path / "must-not-load.py")) + assert not (workspace / "received.txt").exists() + assert not (workspace / ".dsh").exists() + assert f._quota_spend_count(runtime) == 0 + code, executed = f._run_cli([*run_args, "--execute"]) if first_attempt_fails: assert code == 1 and executed["status"] == "failed", executed From 4d69a53c7851377f1a5ae7ceacbb48148cc8e161 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:41:34 +0800 Subject: [PATCH 14/33] refactor(quota): share complete acceptance in the typed contract owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/cli_projection.py | 63 +-------- loopx/control_plane/quota/turn_envelope.ts | 109 +++++++++++++-- .../test_complete_turn_decision.py | 22 ++++ .../test_quota_acceptance_references.py | 124 ++++++++++++++++-- 4 files changed, 236 insertions(+), 82 deletions(-) diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 3312803abf..105998e1bc 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -747,67 +747,10 @@ def _promote_interaction_contract(payload: dict[str, Any]) -> dict[str, Any]: def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any]: - """Share exact JSON copies in this display only; never trim the contract. + """Legacy display adapter; the complete contract uses this same TS owner.""" + from .turn_envelope import _turn_envelope_result - Work context is the preferred full carrier, then the planning horizon. - This Python CLI adapter only changes serialization after owner decisions; - canonical state, captures and replan context construction remain untouched. - """ - paths: list[tuple[str | int, ...]] = [] - interaction = payload.get("interaction_contract") - channel = interaction.get("agent_channel") if isinstance(interaction, dict) else None - context = channel.get("work_context") if isinstance(channel, dict) else None - sources = context.get("sources") if isinstance(context, dict) else None - for index, source in enumerate(sources if isinstance(sources, list) else []): - if (isinstance(source, dict) and source.get("source") == "goal_acceptance" - and isinstance(source.get("content"), dict) - and source["content"].get("status", "loaded") == "loaded" - and isinstance(source["content"].get("goal_acceptance_contract"), dict) - and source["content"]["goal_acceptance_contract"]): - paths.append(("interaction_contract", "agent_channel", "work_context", - "sources", index, "content", "goal_acceptance_contract")) - break - paths.extend([ - ("planning_horizon", "core_goal", "acceptance_contract"), - ("autonomous_replan_obligation", "replan_context", "core_goal", "acceptance_contract"), - ("agent_todo_summary", "goal_acceptance_contract"), - ]) - carriers: dict[str, str] = {} - result = payload - for path in paths: - parent: Any = result - for part in path[:-1]: - if isinstance(parent, dict) and isinstance(part, str): - parent = parent.get(part) - elif isinstance(parent, list) and isinstance(part, int) and part < len(parent): - parent = parent[part] - else: - parent = None - break - field = str(path[-1]) - value = parent.get(field) if isinstance(parent, dict) else None - if not isinstance(value, dict) or not value or field + "_ref" in parent: - continue - # Python equality conflates True/1 and False/0. Compare JSON types and - # the complete content, including revision, scope, failures and sources. - encoded = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False) - reference = carriers.get(encoded) - if reference is None: - carriers[encoded] = "$" + "".join( - f"[{part}]" if isinstance(part, int) else f".{part}" for part in path) - continue - if len(encoded) <= len(reference) + 8: - continue - # Copy only the affected spine, so callers retain their original facts. - result = dict(result) - target: Any = result - for part in path[:-1]: - child = target[part] - target[part] = list(child) if isinstance(child, list) else dict(child) - target = target[part] - del target[field] - target[field + "_ref"] = reference - return result + return _turn_envelope_result("reference_goal_acceptance", payload=payload) def _reference_explore_writeback_guidance(payload: dict[str, Any]) -> dict[str, Any]: diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index e17b9b927b..63234e2cdd 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -249,6 +249,92 @@ function canonicalHash(value: unknown): string { return `sha256:${createHash("sha256").update(JSON.stringify(canonicalValue(value)), "utf8").digest("hex")}`; } +/** Existing acceptance carriers only: this is not a general JSON-path codec. + * The loaded work context is preferred, then the complete planning/replan facts. + */ +function acceptanceCarriers(payload: JsonObject) { + // Keep carrier parents attached to the cloned document. The general object() + // helper intentionally returns copies for projection callers. + const carrierObject = (value: unknown): JsonObject => + typeof value === "object" && value !== null && !Array.isArray(value) + ? value as JsonObject : {}; + const carriers: {parent: JsonObject; field: string; path: string}[] = []; + const sources = carrierObject(carrierObject(carrierObject(payload.interaction_contract).agent_channel).work_context).sources; + if (Array.isArray(sources)) { + const index = sources.findIndex(value => { + const source = carrierObject(value), content = carrierObject(source.content); + return source.source === "goal_acceptance" && + (content.status === undefined || content.status === "loaded") && + Object.keys(carrierObject(content.goal_acceptance_contract)).length > 0; + }); + if (index >= 0) carriers.push({parent: carrierObject(carrierObject(sources[index]).content), + field: "goal_acceptance_contract", + path: `$.interaction_contract.agent_channel.work_context.sources[${index}].content.goal_acceptance_contract`}); + } + carriers.push( + {parent: carrierObject(carrierObject(payload.planning_horizon).core_goal), field: "acceptance_contract", + path: "$.planning_horizon.core_goal.acceptance_contract"}, + {parent: carrierObject(carrierObject(carrierObject(payload.autonomous_replan_obligation).replan_context).core_goal), + field: "acceptance_contract", path: "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract"}, + {parent: carrierObject(payload.agent_todo_summary), field: "goal_acceptance_contract", + path: "$.agent_todo_summary.goal_acceptance_contract"}, + ); + return carriers; +} + +function inlineAcceptance(payload: JsonObject, reference: unknown): JsonObject { + // Older captures may name any loaded work-context carrier, not only the + // preferred first source chosen for new sharing. + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.agent_channel\.work_context\.sources\[(0|[1-9]\d*)\]\.content\.goal_acceptance_contract$/.exec(reference) : null; + if (match) { + const sources = object(object(object(payload.interaction_contract).agent_channel).work_context).sources; + const source = object(Array.isArray(sources) ? sources[Number(match[1])] : null); + const content = object(source.content); + if (source.source === "goal_acceptance" && + (content.status == null || content.status === "loaded") && + Object.keys(object(content.goal_acceptance_contract)).length) { + return object(content.goal_acceptance_contract); + } + } + const carrier = acceptanceCarriers(payload).find(item => item.path === reference); + const value = object(carrier?.parent[carrier.field]); + if (!carrier || !Object.keys(value).length) { + throw new EffectRuntimeRequestError("Goal acceptance reference must resolve to complete inline work context or planning facts"); + } + return value; +} + +function referenceGoalAcceptance(payload: JsonObject): JsonObject { + const result = structuredClone(payload); + const owners = new Map(); + const relocated = new Map(); + const carriers = acceptanceCarriers(result); + for (const {parent, field, path} of carriers) { + const value = object(parent[field]); + if (!Object.keys(value).length || Object.hasOwn(parent, `${field}_ref`)) continue; + // Complete canonical JSON distinguishes booleans/numbers, revisions, + // provenance and negative evidence. Never deduplicate a prose summary. + const encoded = JSON.stringify(canonicalValue(value)); + const owner = owners.get(encoded); + if (!owner) owners.set(encoded, path); + else if (Buffer.byteLength(encoded, "utf8") > Buffer.byteLength(owner, "utf8") + 8) { + delete parent[field]; + parent[`${field}_ref`] = owner; + relocated.set(path, owner); + } + } + // A historical source can already reference a carrier that was just shared. + // Rebind that exact reference to the retained inline owner, never a chain. + for (const {parent, field} of carriers) { + const reference = parent[`${field}_ref`]; + if (typeof reference === "string" && relocated.has(reference)) { + parent[`${field}_ref`] = relocated.get(reference); + } + } + return result; +} + function compactFields( value: unknown, fields: readonly string[], @@ -336,18 +422,9 @@ function replanGoalContext(payload: JsonObject, reads: JsonObject | null, coreRe function indexedGoal(value: unknown): JsonObject { const core = {...object(value)}; if (!("acceptance_contract" in core) && "acceptance_contract_ref" in core) { - const match = typeof core.acceptance_contract_ref === "string" - ? /^\$\.interaction_contract\.agent_channel\.work_context\.sources\[(0|[1-9]\d*)\]\.content\.goal_acceptance_contract$/.exec(core.acceptance_contract_ref) : null; - const sources = object(object(object(payload.interaction_contract).agent_channel).work_context).sources; - const source = object(match && Array.isArray(sources) ? sources[Number(match[1])] : null); - const content = object(source.content); - if (!match || source.source !== "goal_acceptance" - || content.status != null && content.status !== "loaded" - || !Object.keys(object(content.goal_acceptance_contract)).length) { - throw new EffectRuntimeRequestError("Goal acceptance reference must resolve to complete inline work context"); - } + const acceptance = inlineAcceptance(payload, core.acceptance_contract_ref); delete core.acceptance_contract_ref; - core.acceptance_contract = content.goal_acceptance_contract; + core.acceptance_contract = acceptance; } const acceptance = {...object(core.acceptance_contract)}; const requirements = object(acceptance.outcome_requirements); @@ -1191,6 +1268,11 @@ function requireCompleteDecisionSource(source: JsonObject): void { // adapter's unknown-action fallback at the new version boundary. requireStringLiteral(source.effective_action, [...EFFECTIVE_ACTIONS, ...AGENT_SCOPE_FRONTIER_ACTIONS], "Turn effective_action"); + for (const {parent, field} of acceptanceCarriers(source)) { + if (!Object.hasOwn(parent, field) && Object.hasOwn(parent, `${field}_ref`)) { + inlineAcceptance(source, parent[`${field}_ref`]); + } + } } export function requireCompleteTurnDecision(value: unknown): JsonObject { @@ -1273,6 +1355,8 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { })}}}}; } } + source = referenceGoalAcceptance(source); + requireCompleteDecisionSource(source); const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, source_decision_hash: canonicalHash(source)}}; @@ -1381,6 +1465,9 @@ function bindTurnDecisionCapture(request: JsonObject): JsonObject { export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; + if (operation === "reference_goal_acceptance") { + return referenceGoalAcceptance(requireJsonObject(request.payload, "acceptance sharing source")); + } if (operation === "replan_view") return replanView(request); if (operation === "replan_cli_view") { const payload = requireJsonObject(request.payload, "replan CLI payload"); diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index c12ba6a52a..31ea464f7c 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -351,6 +351,23 @@ def test_real_turn_preview_reaches_complete_codex_input(tmp_path, monkeypatch, p initialize_canonical_authority(runtime, f.GOAL_ID, build_todo_runtime_shadow_projection(goal_id=f.GOAL_ID, handoff_mode="soft_claim", todos=summary["agent_todos"]["items"]), state_path=state, provider=provider) + # Add acceptance through the real authority API, so native execution and + # failed-turn recovery exercise sharing rather than a hand-built contract. + prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] + document = tmp_path / "acceptance.json" + document.write_text(json.dumps({"outcome_id": "complete-input", "owner_agent_id": f.AGENT_ID, + "objective": "Deliver the complete requested artifact", "todo_ids": [f.TODO_ID], + "requirements": [{"id": "complete", "description": "Preserve the original content and tail constraint."}], + "source": {"ref": "message:owner", "summary": "Original artifact requirement"}})) + code, inspected = f._run_cli([*prefix, "goal-acceptance", "inspect", "--goal-id", f.GOAL_ID]) + assert code == 0, inspected + code, refined = f._run_cli([*prefix, "goal-acceptance", "refine", "--goal-id", f.GOAL_ID, + "--agent-id", f.AGENT_ID, "--document", str(document), "--expected-provider-revision", + inspected["provider_revision"], "--operation-id", "original-acceptance", "--execute"]) + assert code == 0, refined + code, inspected = f._run_cli([*prefix, "goal-acceptance", "inspect", "--goal-id", f.GOAL_ID]) + assert code == 0, inspected + expected_acceptance = inspected["goal_acceptance_contract"] args = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", "turn", "plan", "--goal-id", f.GOAL_ID, "--agent-id", f.AGENT_ID, "--turn-instance-id", "complete-source", "--host", "codex-cli", @@ -472,6 +489,11 @@ def future_action(payload, **kwargs): observed = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) assert observed["work_contract"]["schema_version"] == "loopx_turn_envelope_v1" assert "turn_envelope" not in observed + acceptance_sources = observed["work_contract"]["interaction_contract"]["agent_channel"]["work_context"]["sources"] + acceptance_index = next(i for i, row in enumerate(acceptance_sources) if row["source"] == "goal_acceptance") + assert acceptance_sources[acceptance_index]["content"]["goal_acceptance_contract"] == expected_acceptance + assert observed["work_contract"]["agent_todo_summary"]["goal_acceptance_contract_ref"] == ( + f"$.interaction_contract.agent_channel.work_context.sources[{acceptance_index}].content.goal_acceptance_contract") saved = list(runtime.rglob("decisions/*.json")) assert len(saved) == 1 saved_bytes = saved[0].read_bytes() diff --git a/tests/control_plane/test_quota_acceptance_references.py b/tests/control_plane/test_quota_acceptance_references.py index 1aa0f17eaf..f354624411 100644 --- a/tests/control_plane/test_quota_acceptance_references.py +++ b/tests/control_plane/test_quota_acceptance_references.py @@ -1,4 +1,4 @@ -"""Display sharing preserves the complete acceptance and executable decisions.""" +"""One complete acceptance survives composition, display and actual admission.""" from copy import deepcopy import json @@ -8,12 +8,18 @@ _reference_inline_goal_acceptance, compact_quota_should_run_cli_payload, ) from loopx.control_plane.quota.turn_envelope import ( - build_turn_envelope, quota_action_signature_document, + build_turn_envelope, compose_turn_decision, quota_action_signature_document, turn_envelope_action_signature_document, ) from test_replan_host_context_projection import _quota_payload +@pytest.fixture(params=[_reference_inline_goal_acceptance, compose_turn_decision], + ids=["legacy-display", "complete-composition"]) +def share(request): + return request.param + + def acceptance(): return {"revision": 3, "scope": {"kind": "all_advancement"}, "status": "failed", "requirements": [{"id": "original", "text": "Keep the complete original requirement. " * 20}, @@ -38,12 +44,12 @@ def packet(with_context=True): @pytest.mark.parametrize("with_context", [False, True]) -def test_exact_copies_share_one_complete_source_without_changing_signed_action(with_context): +def test_exact_copies_share_one_complete_source_without_changing_signed_action(with_context, share): source = packet(with_context) original = deepcopy(source) - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert source == original - assert _reference_inline_goal_acceptance(shared) == shared + assert share(shared) == shared ref = ("$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract" if with_context else "$.planning_horizon.core_goal.acceptance_contract") assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == ref @@ -64,7 +70,7 @@ def test_exact_copies_share_one_complete_source_without_changing_signed_action(w @pytest.mark.parametrize("change", ["revision", "scope", "failure", "type", "source"]) -def test_different_contracts_are_retained_in_full(change): +def test_different_contracts_are_retained_in_full(change, share): source = packet(False) changed = source["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] if change == "revision": @@ -77,27 +83,27 @@ def test_different_contracts_are_retained_in_full(change): changed["verification"]["passed"] = 0 # False == 0 in Python if change == "source": changed["sources"][0]["ref"] = "message:corrected" - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert shared["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] == changed assert "acceptance_contract_ref" not in shared["autonomous_replan_obligation"]["replan_context"]["core_goal"] @pytest.mark.parametrize("missing", [None, {}, [], "unavailable"]) -def test_missing_or_malformed_carriers_never_create_dangling_references(missing): +def test_missing_or_malformed_carriers_never_create_dangling_references(missing, share): source = packet() source["interaction_contract"]["agent_channel"]["work_context"] = missing source["planning_horizon"] = missing source["autonomous_replan_obligation"] = missing - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert shared["agent_todo_summary"]["goal_acceptance_contract"] == acceptance() assert "goal_acceptance_contract_ref" not in shared["agent_todo_summary"] -def test_unavailable_context_is_not_used_even_if_it_retains_old_content(): +def test_unavailable_context_is_not_used_even_if_it_retains_old_content(share): source = packet() content = source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"] content["status"] = "unavailable" - shared = _reference_inline_goal_acceptance(source) + shared = share(source) assert shared["planning_horizon"]["core_goal"]["acceptance_contract"] == acceptance() assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == "$.planning_horizon.core_goal.acceptance_contract" assert build_turn_envelope(shared)["action"]["planning_horizon"]["core_goal"]["acceptance_contract"] == acceptance() @@ -165,3 +171,99 @@ def test_real_cli_reuses_complete_acceptance_for_selection_and_replan(tmp_path, envelope = build_turn_envelope(selected) assert envelope["work_context"]["sources"][source_index]["content"]["goal_acceptance_contract"] == expected assert envelope["action_signature"]["matches"] is True + + +@pytest.mark.parametrize("mutation", ["dangling", "cycle", "self", "unavailable"]) +def test_complete_readers_reject_dangling_or_cyclic_acceptance_even_with_valid_hash(mutation): + from test_complete_turn_decision import _resign_contract, _canonical_digest + from loopx.control_plane.quota.turn_envelope import _turn_envelope_result + from loopx.control_plane.turn_driver.codex_cli import _prompt + contract = compose_turn_decision(packet()) + summary = contract["agent_todo_summary"] + if mutation == "dangling": + summary["goal_acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[9].content.goal_acceptance_contract" + elif mutation == "self": + summary["goal_acceptance_contract_ref"] = "$.agent_todo_summary.goal_acceptance_contract" + elif mutation == "cycle": + summary["goal_acceptance_contract_ref"] = "$.planning_horizon.core_goal.acceptance_contract" + contract["planning_horizon"]["core_goal"]["acceptance_contract_ref"] = "$.agent_todo_summary.goal_acceptance_contract" + else: + sources = contract["interaction_contract"]["agent_channel"]["work_context"]["sources"] + sources.append(deepcopy(sources[0])) + sources[1]["content"]["status"] = "unavailable" + summary["goal_acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[1].content.goal_acceptance_contract" + contract, _ = _resign_contract(contract) + for read in ( + lambda: compose_turn_decision(contract), + lambda: build_turn_envelope(contract), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review"), + lambda: _prompt({"work_contract": contract}), + ): + with pytest.raises(RuntimeError, match="acceptance reference"): + read() + + +@pytest.mark.parametrize("status", [None, "loaded"]) +def test_historical_reference_to_later_loaded_source_keeps_full_material(status): + from test_complete_turn_decision import _canonical_digest + from loopx.control_plane.quota.turn_envelope import _turn_envelope_result + source = packet() + sources = source["interaction_contract"]["agent_channel"]["work_context"]["sources"] + sources.append(deepcopy(sources[0])) + sources[1]["content"]["status"] = status + sources[1]["content"]["goal_acceptance_contract"]["revision"] = 7 + expected = deepcopy(sources[1]["content"]["goal_acceptance_contract"]) + core = source["autonomous_replan_obligation"]["replan_context"]["core_goal"] + del core["acceptance_contract"] + core["acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[1].content.goal_acceptance_contract" + contract = compose_turn_decision(source) + view = _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review") + # The staged review indexes requirements, retaining the complete captured + # requirement source, revision and negative verification for exact readback. + assert view["core_goal"]["acceptance_contract"]["revision"] == 7 + assert contract["interaction_contract"]["agent_channel"]["work_context"]["sources"][1]["content"]["goal_acceptance_contract"] == expected + + +def test_shared_contract_survives_warm_delta_and_missing_baseline_recovery(): + from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery + initial = compose_turn_decision(packet()) + changed = packet() + changed["reason"] = "New feedback retains the original requirements and stops." + current = compose_turn_decision(changed) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(initial, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(current, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(current, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == "delta" + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == current + assert compose_turn_decision(restored) == current + assert restored["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() + + +@pytest.mark.parametrize("value", [{"revision": 1}, {"note": "短"}, {"passed": False}]) +def test_small_identical_contracts_stay_inline_when_a_reference_would_cost_more(value, share): + source = packet(False) + source["planning_horizon"]["core_goal"]["acceptance_contract"] = deepcopy(value) + source["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] = deepcopy(value) + source["agent_todo_summary"]["goal_acceptance_contract"] = deepcopy(value) + shared = share(source) + assert shared["agent_todo_summary"]["goal_acceptance_contract"] == value + assert shared["autonomous_replan_obligation"]["replan_context"]["core_goal"]["acceptance_contract"] == value + + +@pytest.mark.parametrize("existing_owner", ["planning", "replan"]) +def test_existing_reference_follows_a_relocated_complete_owner(existing_owner, share): + source = packet() + del source["agent_todo_summary"]["goal_acceptance_contract"] + owner = ("$.planning_horizon.core_goal.acceptance_contract" if existing_owner == "planning" + else "$.autonomous_replan_obligation.replan_context.core_goal.acceptance_contract") + source["agent_todo_summary"]["goal_acceptance_contract_ref"] = owner + shared = share(source) + assert shared["agent_todo_summary"]["goal_acceptance_contract_ref"] == ( + "$.interaction_contract.agent_channel.work_context.sources[0].content.goal_acceptance_contract") + assert compose_turn_decision(shared)["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["goal_acceptance_contract"] == acceptance() From a3a82d62b55ae2b092317176894be73371501ca3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:44:40 +0800 Subject: [PATCH 15/33] docs(rfc): require code-path convergence in each migration slice Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 47 +++++++++++++++++-- ...ller-executor-unified-contract-v0.zh-CN.md | 31 ++++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 4c9b6074ab..7ba80bdd62 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -567,6 +567,22 @@ are reconciled. Rollback likewise separates outstanding work from new admission. A version transition does not rewrite saved prompts, journals or source hashes as if they had always used the new contract. +Code convergence is part of each migration slice, not only the final C9 cleanup. +Both entrypoints must call the same semantic implementation for composition, +reference validation, context reconstruction and settlement. Keep genuinely +host-specific launch/observation mechanics in thin adapters; do not force their +different lifecycles into a new general-purpose runner. The existing typed owners +remain responsible for their respective decisions. + +For each slice, record the before/after callsites, shared owner, removed duplicate +algorithm and remaining compatibility caller with its removal condition. Qualify +both routes with the same independently authored semantic cases and their real +transport/recovery tests. A wrapper that only delegates can be transitional; +copying a rule into a newly named common module while leaving two implementations +is not convergence. The next change to a shared rule should be made and tested in +one owner and reach both routes. Do not make declining line counts an acceptance +metric or remove unique host coverage to meet one. + The first C1 PR must include the two production composition callsites above and at least one complete producer-to-final-input journey, with an explicit list of remaining readers. Qualifying that bounded slice does not enable the new writer @@ -669,10 +685,33 @@ Extra acceptance/Explore reads, actual request framing, cache behavior and outcome differences are unmeasured. The legacy receipt toggle produced identical data and does not add an independent workload. No net-benefit conclusion follows. -The next implementation slice should first qualify exact same-contract sharing -through composition, readers and recovery, then assess deferred-detail choices -with the matched workload above. Keep cold and staged totals separate and retain -negative results; do not force a byte target by dropping unique instructions. +The acceptance-sharing increment in #7430 moves the original Python display +algorithm into the existing TypeScript composition owner, before hashing/capture; +the old display helper now delegates to that same implementation. Complete readers +and historical staged reads use one reference resolver. Equal contracts retain +one full owner; different revisions, JSON types, provenance and negative evidence +remain distinct. Existing references follow relocated owners. Dangling, cyclic +or unavailable references reject even with valid integrity hashes. Small objects +stay inline when a reference would cost more. + +Isolated File/SQLite quota and native-host fixtures cover complete acceptance at +the actual synthetic Codex subprocess input, failed-attempt recovery and original +once-only settlement; shared full/delta/lost-baseline reconstruction is also +exercised. These qualify transport semantics, not model compliance. The legacy +display adapter adds a transport call; complete composition shares inside its +existing call. No latency improvement is claimed. + +A fixed-source acceptance fixture with four identical carriers produces 23,432 +compact UTF-8 JSON bytes for the fused contract versus 22,237 for the original +non-staged CLI (+5.37%). With capture staging, fused bytes are 23,515 versus +24,509 for original entry/review/writeback together, excluding extra material +reads. The original display-sharing helper is frozen at `895f38fc5` for this +comparison. The acceptance-free workload above remains unchanged. This absorbs +an existing quota efficiency into the common path; it does not establish an +advantage over quota or full-cost benefit. Next qualify the remaining vision, +authoring and guidance sharing in their existing owners before deferred-detail +choices. Keep cold and staged totals separate and retain negative results; do +not force a byte target by dropping unique instructions. Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 1a201c51f5..a6c5f80112 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -428,6 +428,16 @@ Draft,不是仅等待评审。尚无模型或成本收益证据。历史 effec 受支持恢复窗口核清前保留历史 decoder。回退同样区分未结工作与新准入。版本迁移不能 把保存的 prompt、journal 或源 hash 改写成仿佛一直使用新契约。 +代码收敛属于每个迁移切片,不只留给最终 C9 清理。两个入口须共用 composition、引用 +校验、上下文重建与结算的语义实现。真实的宿主 launch/observation 差异留在薄 adapter; +不为强行统一不同生命周期而另造通用 runner。各项决策仍由既有 typed owner 负责。 + +每个切片记录迁移前后调用点、共享 owner、已删除的重复算法,以及剩余兼容 caller 和 +退出条件。两条路径共用独立编写的语义用例,并分别验证真实传输与恢复。仅委托调用的 +wrapper 可以过渡保留;给公共模块起新名字却继续保留两套规则实现,不算收敛。下一次 +修改共同规则,应只改、只测一个 owner 并传递到两条路径。代码行数下降不是验收指标, +也不能为此删除宿主独有的验证覆盖。 + C1 首个 PR 必须覆盖上述两个生产组合调用点,并交付至少一条从生产端到最终输入的 完整旅程,同时明确剩余 reader。这个有界切片通过资格,不代表可向未验收 host 开启 新 writer,也不关闭 C1。最终切换须在全部受支持 reader 验收后删除活跃格式选择。 @@ -508,9 +518,24 @@ projection 为 20,543,完整候选为 21,738:**相对 CLI 增加 5.82%**, 及成果差异均未测。切换 legacy receipt 得到相同数据,不增加独立 workload。这些数字 不支持净收益结论。 -下一实施切片应先在 composition、reader 和 recovery 中验证同契约精确共享,再用上述 -可比负载评估延后详情的取舍。冷启动与分阶段总量分别保留,保留负结果,不能靠删除独有 -指令达到字节目标。 +#7430 的验收共享增量把原 Python 展示算法移入既有 TypeScript composition owner, +在 hash/capture 前执行;旧展示 helper 只委托同一实现。完整契约 reader 与历史分阶段 +读取共用一个引用解析器。相同契约保留一份完整 owner;不同 revision、JSON 类型、来源 +和负面证据分别保留。已有引用随 owner 的迁移同步调整;即使完整性 hash 正确,悬空、 +循环或不可用引用也会拒绝。引用比原对象更大时仍保留短对象原文。 + +隔离 File/SQLite 的 quota 和 native host fixture 覆盖真实合成 Codex 子进程输入中的 +完整验收、失败后恢复和原身份一次性结算,也验证共享后的 full/delta/丢失基线重建。 +这些证明传输语义,不证明模型遵从。旧展示 adapter 增加一次传输调用;完整契约在原有 +composition 调用内部完成共享。未声称延迟改善。 + +固定 source 的四份相同验收 fixture 中,融合契约为 23,432 个紧凑 UTF-8 JSON 字节, +原未分阶段 CLI 为 22,237(+5.37%)。启用 capture 分阶段时,融合契约为 23,515, +原 entry/review/writeback 合计为 24,509,未计追加材料读取。对照冻结 `895f38fc5` +的原展示共享 helper;上述不含验收副本的 workload 数字不变。这把 quota 已有的效率 +吸收到共同路径,不能证明优于 quota 或完整成本获益。下一步先在既有 owner 验证剩余 +vision、authoring 和 guidance 共享,再评估延后详情。冷启动与分阶段总量分别保留, +保留负结果,不能靠删除独有指令达到字节目标。 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 From ac21bb5c466703ffe0d6593350260bea069e85cd Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:14:02 +0800 Subject: [PATCH 16/33] refactor(quota): share complete replan authoring through one owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/cli_projection.py | 39 +--- loopx/control_plane/quota/turn_envelope.ts | 53 +++++- .../test_complete_turn_decision.py | 9 + .../test_effective_turn_replan.py | 9 +- .../test_quota_authoring_references.py | 172 ++++++++++++++++++ 5 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 tests/control_plane/test_quota_authoring_references.py diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 105998e1bc..7262c75b88 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -444,43 +444,20 @@ def _compact_vision_continuation_audit( def _reference_inline_replan_authoring( packet: dict[str, Any], *, payload: dict[str, Any] ) -> dict[str, Any]: - """Reference an identical executable input already present in this response.""" + """Legacy display metadata around the shared complete authoring owner.""" + from .turn_envelope import _turn_envelope_result - writeback = packet.get("writeback_contract") - if not isinstance(writeback, dict) or not isinstance( - writeback.get("vision_authoring"), dict - ): - return packet - interaction = payload.get("interaction_contract") - cli = interaction.get("cli_channel") if isinstance(interaction, dict) else None - plan = cli.get("settlement_plan") if isinstance(cli, dict) else None - steps = plan.get("ordered_steps") if isinstance(plan, dict) else None - if not isinstance(steps, list): - return packet - index = next( - (index for index, step in enumerate(steps) - if isinstance(step, dict) and step.get("kind") == "durable_writeback" - and step.get("vision_authoring") == writeback["vision_authoring"]), - None, - ) - if index is None: + shared = _turn_envelope_result("reference_replan_authoring", + payload={**payload, "replan_action_packet": packet})["replan_action_packet"] + if shared == packet: return packet - reference = ( - "$.interaction_contract.cli_channel.settlement_plan.ordered_steps" - f"[{index}].vision_authoring" - ) - compact_writeback = dict(writeback) - compact_writeback.pop("vision_authoring") - compact_writeback["vision_authoring_ref"] = reference - compact = dict(packet) - compact["writeback_contract"] = compact_writeback - compact["payload_compaction"] = { + reference = shared["writeback_contract"]["vision_authoring_ref"] + return {**shared, "payload_compaction": { "schema_version": QUOTA_CLI_REPLAN_ACTION_COMPACTION_SCHEMA_VERSION, "mode": "compact_hot_path", "compacted_fields": ["writeback_contract.vision_authoring"], "projection_ref": reference, - } - return compact + }} def _vision_continuation_ref(audit: dict[str, Any]) -> dict[str, Any]: diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 63234e2cdd..299cd6beb2 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -335,6 +335,47 @@ function referenceGoalAcceptance(payload: JsonObject): JsonObject { return result; } +/** Reuse the existing settlement authoring owner, never a summary or a chain. */ +function replanAuthoringContract(payload: JsonObject): JsonObject { + const contract = object(object(payload.replan_action_packet).writeback_contract); + if (!Object.hasOwn(contract, "vision_authoring_ref")) return contract; + const reference = contract.vision_authoring_ref; + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.cli_channel\.settlement_plan\.ordered_steps\[(0|[1-9]\d*)\]\.vision_authoring$/.exec(reference) : null; + const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; + const step = object(match && Array.isArray(steps) ? steps[Number(match[1])] : null); + if (Object.hasOwn(contract, "vision_authoring") || step.kind !== "durable_writeback" || + Object.hasOwn(step, "vision_authoring_ref") || !Object.keys(object(step.vision_authoring)).length) { + throw new EffectRuntimeRequestError("Replan authoring reference must resolve to one complete inline durable-writeback owner"); + } + delete contract.vision_authoring_ref; + return {...contract, vision_authoring: step.vision_authoring}; +} + +function referenceReplanAuthoring(payload: JsonObject): JsonObject { + const packet = object(payload.replan_action_packet); + const contract = object(packet.writeback_contract); + if (Object.hasOwn(contract, "vision_authoring_ref")) { + replanAuthoringContract(payload); + return payload; + } + const authoring = object(contract.vision_authoring); + const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; + if (!Object.keys(authoring).length || !Array.isArray(steps)) return payload; + const encoded = JSON.stringify(canonicalValue(authoring)); + const index = steps.findIndex(value => { + const step = object(value); + return step.kind === "durable_writeback" && !Object.hasOwn(step, "vision_authoring_ref") && + JSON.stringify(canonicalValue(step.vision_authoring)) === encoded; + }); + if (index < 0) return payload; + const reference = `$.interaction_contract.cli_channel.settlement_plan.ordered_steps[${index}].vision_authoring`; + if (Buffer.byteLength(encoded, "utf8") <= Buffer.byteLength(reference, "utf8") + 8) return payload; + delete contract.vision_authoring; + return {...payload, replan_action_packet: {...packet, writeback_contract: { + ...contract, vision_authoring_ref: reference}}}; +} + function compactFields( value: unknown, fields: readonly string[], @@ -557,7 +598,7 @@ function replanView(request: JsonObject): JsonObject { // duplicate context are available in the unchanged full captured source. const stride = {...object(packet.stride_control)}; for (const key of ["decision_followthrough", "explore_basis", "decision_value_review", "minimal_example"]) delete stride[key]; - const contract = {...object(packet.writeback_contract)}; + const contract = replanAuthoringContract(payload); if (contract.vision_authoring) { const authoring = {...object(contract.vision_authoring)}; delete authoring.minimal_example; @@ -639,7 +680,7 @@ function replanActionPacket(payload: JsonObject, reads = replanReadCommands(payl // The typed replan owner supplies bounded instructions; do not truncate their // authority/stop qualifiers through generic diagnostic compaction. if (Array.isArray(source.planning_guidance)) compact.planning_guidance = source.planning_guidance; - const writeback = object(source.writeback_contract); + const writeback = replanAuthoringContract(payload); if (writeback.vision_authoring) compact.writeback_contract = writeback; return Object.keys(compact).length > 0 ? compact : null; } @@ -1263,6 +1304,7 @@ function requireCompleteDecisionSource(source: JsonObject): void { // Validate executable semantics with their owner, retaining the original // object (including optional facts) rather than its normalized decode. decodeInteractionContract(source.interaction_contract); + replanAuthoringContract(source); // This root field selects execution, unlike optional evidence carried intact // in the source. Reuse both registered owners; do not inherit the historical // adapter's unknown-action fallback at the new version boundary. @@ -1340,7 +1382,7 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { // before capture too. A complete contract must not advertise an attachment // that requires a Todo when this admission has only an obligation. Reuse the // exact known-guidance check; custom instructions and evidence stay intact. - // Do not run the historical compactor: supported authoring stays fully inline. + // Do not run the historical compactor: complete authoring remains in this capture. if (unboundExploreReplan(source, plan)) { const currentInteraction = object(source.interaction_contract); const agent = object(currentInteraction.agent_channel); @@ -1355,7 +1397,7 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { })}}}}; } } - source = referenceGoalAcceptance(source); + source = referenceReplanAuthoring(referenceGoalAcceptance(source)); requireCompleteDecisionSource(source); const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, @@ -1465,6 +1507,9 @@ function bindTurnDecisionCapture(request: JsonObject): JsonObject { export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; + if (operation === "reference_replan_authoring") { + return referenceReplanAuthoring(requireJsonObject(request.payload, "authoring sharing source")); + } if (operation === "reference_goal_acceptance") { return referenceGoalAcceptance(requireJsonObject(request.payload, "acceptance sharing source")); } diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index 31ea464f7c..d19c38fd2a 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -580,6 +580,15 @@ def test_native_replan_complete_input_retains_verified_material_readers(tmp_path review = json.loads((project / "review.json").read_text()) writeback = json.loads((project / "writeback.json").read_text()) assert review["context_id"] == writeback["context_id"] == context["context_id"] + # The native outer controller owns settlement; its model input has no + # inline settlement owner to reference. Preserve all authoring here. + contract = source["replan_action_packet"]["writeback_contract"] + steps = source["interaction_contract"]["cli_channel"].get("settlement_plan", {}).get("ordered_steps", []) + assert not any("vision_authoring" in step for step in steps) + assert "vision_authoring_ref" not in contract + if "vision_authoring" in contract: + assert writeback["writeback_contract"]["vision_authoring"] == { + key: value for key, value in contract["vision_authoring"].items() if key != "minimal_example"} assert review["source_hash"] == writeback["source_hash"] == request["source_decision_hash"] assert "Read detail_ref.replan_action_packet.review" in prompt assert "before reading detail_ref.replan_action_packet.writeback" in prompt diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index b0c72e7fea..0fbc0b6be9 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -273,8 +273,13 @@ def periodic(): ) assert rc == 0, envelope assert envelope["replan_action_packet"]["planning_guidance"] == selected["replan_action_packet"]["planning_guidance"] - assert envelope["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring - assert envelope["writeback"]["settlement_plan"] == plan + # Both CLI spellings now emit the same complete contract. Historical + # adapters still reconstruct the authoring body from that capture. + assert envelope["schema_version"] == selected["schema_version"] == "loopx_turn_envelope_v1" + assert envelope["replan_action_packet"]["writeback_contract"] == contract + assert envelope["interaction_contract"]["cli_channel"]["settlement_plan"] == plan + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + assert build_turn_envelope(envelope)["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring vision_path = tmp_path / "periodic-vision.json" vision = json.loads(json.dumps(authoring["minimal_example"])) vision["vision_patch"] = {"acceptance_summary": "Original acceptance remains open."} diff --git a/tests/control_plane/test_quota_authoring_references.py b/tests/control_plane/test_quota_authoring_references.py new file mode 100644 index 0000000000..841f9a7eb6 --- /dev/null +++ b/tests/control_plane/test_quota_authoring_references.py @@ -0,0 +1,172 @@ +"""Replan authoring shares complete settlement instructions without losing stops.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import _reference_inline_replan_authoring +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, build_turn_envelope, compose_turn_decision, + quota_action_signature_document, prepare_context_delivery, restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + authoring = {"schema_version": "goal_vision_replan_contract_v0", + "fields": {"vision_patch": {"acceptance_summary": 420}}, + "rules": ["Keep all original requirements and cite source evidence. " * 12, + "A result does not grant publication or spending authority."], + "minimal_example": {"state": "vision_patch_proposed"}, + "provenance": {"ref": "message:original", "revision": 3}, "required": False} + source["replan_action_packet"]["writeback_contract"]["vision_authoring"] = deepcopy(authoring) + steps = [{"kind": "validation"}, {"kind": "durable_writeback"}] + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}, "ordered_steps": steps} + index = next(i for i, step in enumerate(steps) if step["kind"] == "durable_writeback") + steps[index]["vision_authoring"] = authoring + return source, authoring, index + + +def legacy(source): + return {**source, "replan_action_packet": _reference_inline_replan_authoring( + source["replan_action_packet"], payload=source)} + + +@pytest.fixture(params=[legacy, compose_turn_decision], ids=["legacy-display", "complete"]) +def share(request): + return request.param + + +def test_exact_authoring_keeps_single_complete_owner_and_original_settlement(share): + source, authoring, index = packet() + original = deepcopy(source) + shared = share(source) + assert source == original + assert share(shared) == shared + writeback = shared["replan_action_packet"]["writeback_contract"] + assert "vision_authoring" not in writeback + assert writeback["vision_authoring_ref"] == ( + f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{index}].vision_authoring") + assert shared["interaction_contract"]["cli_channel"]["settlement_plan"] == original["interaction_contract"]["cli_channel"]["settlement_plan"] + if share is legacy: + assert quota_action_signature_document(shared) == quota_action_signature_document(source) + envelope = build_turn_envelope(shared) + assert envelope["action_signature"]["matches"] is True + assert envelope["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring + writer = _turn_envelope_result("replan_view", payload=shared, + source_hash=envelope["action_signature"]["source_decision_hash"], stage="writeback") + expected = {k: v for k, v in authoring.items() if k != "minimal_example"} + assert writer["writeback_contract"]["vision_authoring"] == expected + assert "vision_authoring_ref" not in writer["writeback_contract"] + + +@pytest.mark.parametrize("mutation", ["different-rule", "revision", "type", "wrong-step", "short"]) +def test_distinct_or_unavailable_authoring_is_not_replaced(mutation, share): + source, _, index = packet() + owner = source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][index] + if mutation == "different-rule": + owner["vision_authoring"]["rules"].append("Retain an additional stop.") + elif mutation == "revision": + owner["vision_authoring"]["provenance"]["revision"] = 4 + elif mutation == "type": + owner["vision_authoring"]["required"] = 0 + elif mutation == "wrong-step": + owner["kind"] = "validation" + else: + owner["vision_authoring"] = {"required": True} + source["replan_action_packet"]["writeback_contract"]["vision_authoring"] = {"required": True} + original = deepcopy(source["replan_action_packet"]["writeback_contract"]["vision_authoring"]) + shared = share(source) + assert shared["replan_action_packet"]["writeback_contract"]["vision_authoring"] == original + assert "vision_authoring_ref" not in shared["replan_action_packet"]["writeback_contract"] + + +@pytest.mark.parametrize("mutation", ["dangling", "wrong-step", "chain", "both", "malformed"]) +def test_invalid_authoring_reference_is_rejected_even_with_valid_content_hash(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + source, _, index = packet() + contract = compose_turn_decision(source) + writeback = contract["replan_action_packet"]["writeback_contract"] + writeback.pop("vision_authoring", None) + reference = f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{index}].vision_authoring" + writeback["vision_authoring_ref"] = reference + owner = contract["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][index] + if mutation == "dangling": + writeback["vision_authoring_ref"] = reference.replace(f"[{index}]", "[999]") + elif mutation == "wrong-step": + owner["kind"] = "validation" + elif mutation == "chain": + owner.pop("vision_authoring") + owner["vision_authoring_ref"] = "$.replan_action_packet.writeback_contract.vision_authoring" + elif mutation == "both": + writeback["vision_authoring"] = {"rules": ["Conflicting authority"]} + else: + writeback["vision_authoring_ref"] = {"path": reference} + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract}), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="writeback")): + with pytest.raises(RuntimeError, match="authoring reference"): + read() + + +def test_full_delta_and_lost_baseline_restore_authoring_without_guard_reentry(): + # Use the production delivery/restore owner; independent material is checked + # after reconstruction rather than only comparing signatures. + source, authoring, _ = packet() + before = compose_turn_decision(source) + source["replan_action_packet"]["uncovered_frontier"] = ["A newly observed gap"] + after = compose_turn_decision(source) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(before, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == "delta" + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == after + assert build_turn_envelope(restored)["replan_action_packet"]["writeback_contract"]["vision_authoring"] == authoring + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_real_capture_shares_authoring_and_refuses_invalid_recovery_without_effects(tmp_path, monkeypatch, provider): + import json + import subprocess + import sys + from pathlib import Path + from test_turn_replan_writeback import _fixture + from test_quota_settlement_cli import _run_cli, _spend_run_count, GOAL_ID, AGENT_ID, SELECTED_REPLAN_TODO_ID + project, runtime, registry, state, turn = _fixture(tmp_path, monkeypatch, provider) + capture = tmp_path / "captured" + code, source = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", SELECTED_REPLAN_TODO_ID, + "--turn-instance-id", turn, "--decision-output-dir", str(capture), cwd=project) + assert code == 0, source + assert json.loads((capture / "decision.json").read_text()) == source + contract = source["replan_action_packet"]["writeback_contract"] + assert "vision_authoring" not in contract + assert contract["vision_authoring_ref"] == "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[1].vision_authoring" + original = source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["vision_authoring"] + envelope = build_turn_envelope(source) + import shlex + read = subprocess.run(shlex.split(envelope["replan_action_packet"]["writeback"]), + capture_output=True, text=True, check=True) + assert json.loads(read.stdout)["writeback_contract"]["vision_authoring"] == { + key: value for key, value in original.items() if key != "minimal_example"} + state_before = state.read_bytes() + invalid = deepcopy(source) + invalid["replan_action_packet"]["writeback_contract"]["vision_authoring_ref"] = "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[99].vision_authoring" + invalid, _ = _resign_contract(invalid) + saved = tmp_path / "invalid.json" + saved.write_text(json.dumps(invalid)) + result = subprocess.run([str(Path(sys.executable).with_name("loopx")), "--format", "json", "quota", "replan-view", + "--decision-file", str(saved), "--source-hash", _canonical_digest(invalid), "--replan-stage", "writeback"], + capture_output=True, text=True) + assert result.returncode != 0 + assert "authoring reference" in result.stdout + result.stderr + assert state.read_bytes() == state_before + assert _spend_run_count(runtime) == 0 From 41f50a1207ee260efa10b898a60be32d72e02b82 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:14:02 +0800 Subject: [PATCH 17/33] docs(rfc): record authoring convergence and quota comparison limits Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 35 +++++++++++++++++-- ...ller-executor-unified-contract-v0.zh-CN.md | 21 ++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 7ba80bdd62..f559dc9aef 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -708,11 +708,40 @@ non-staged CLI (+5.37%). With capture staging, fused bytes are 23,515 versus reads. The original display-sharing helper is frozen at `895f38fc5` for this comparison. The acceptance-free workload above remains unchanged. This absorbs an existing quota efficiency into the common path; it does not establish an -advantage over quota or full-cost benefit. Next qualify the remaining vision, -authoring and guidance sharing in their existing owners before deferred-detail -choices. Keep cold and staged totals separate and retain negative results; do +advantage over quota or full-cost benefit. Next qualify the remaining vision +and guidance sharing in their existing owners before deferred-detail choices. Keep cold and staged totals separate and retain negative results; do not force a byte target by dropping unique instructions. +The next #7430 increment shares replan vision-authoring instructions through +that same TypeScript composition boundary. Its retained owner is the existing +durable-writeback settlement step. The legacy Python display helper delegates +sharing and only adds its historical presentation metadata. The complete +reader and historical unstaged/staged adapters resolve the same exact reference; +no second authoring policy is introduced. JSON type, revision or instruction +differences stay inline, and a reference must be smaller. Conflicting inline/ref +values, wrong step kinds, missing targets and chains reject even if hashes match. +The settlement plan, all original instructions and the example remain intact in +the capture; the historical writeback stage retains its existing example omission. + +Two old CLI spellings now expose the same shared complete contract; material +readback, rejection before effects and native input are qualified separately. +The exercised native outer-controller inputs lack an inline settlement authoring +owner, so their authoring +remains inline; sharing never invents that owner or transfers settlement authority. +Four legacy projection tests still expect multiline-title or Explore-guidance +sharing absent from the complete candidate, and fail at both the prior branch +head and this increment. They remain visible C1 migration gaps, not waived gates. +This slice does not settle those consumers or qualify installed App adoption. + +For the fixed synthetic authoring fixture, the original non-staged CLI is 22,062 +compact UTF-8 JSON bytes and the fused contract is 23,266 (+5.46%). With capture +staging, original entry/review/writeback total 24,311 versus 23,349 fused bytes. +The original CLI projection module is frozen at `895f38fc5`; inputs and projected +read stages are held fixed. Additional reads, request framing, tokens/cache, +latency and model outcomes remain unmeasured. Sharing eliminates a duplicate +implementation and duplicate instructions in the candidate, but does not prove +an improvement over quota's existing sharing or a positive full-cost diff. + Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, latency, failures/retries, verification and human intervention. A smaller first diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index a6c5f80112..f6cc41b181 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -534,9 +534,28 @@ composition 调用内部完成共享。未声称延迟改善。 原 entry/review/writeback 合计为 24,509,未计追加材料读取。对照冻结 `895f38fc5` 的原展示共享 helper;上述不含验收副本的 workload 数字不变。这把 quota 已有的效率 吸收到共同路径,不能证明优于 quota 或完整成本获益。下一步先在既有 owner 验证剩余 -vision、authoring 和 guidance 共享,再评估延后详情。冷启动与分阶段总量分别保留, +vision 和 guidance 共享,再评估延后详情。冷启动与分阶段总量分别保留, 保留负结果,不能靠删除独有指令达到字节目标。 +#7430 后续增量通过同一个 TypeScript composition 边界共享 replan 的 vision-authoring +指令,保留的 owner 是既有 durable-writeback 结算步骤。旧 Python 展示 helper 委托 +共享判断,仅添加历史展示元数据。完整 reader 和历史未分阶段/分阶段 adapter 解析同一个 +精确引用,不另建 authoring 策略。JSON 类型、版本或指令不同则保留原文,引用必须更小。 +即使 hash 匹配,冲突的原文/引用、错误步骤类型、缺失目标和引用链也会拒绝。 +结算计划、全部原指令和示例保留在 capture 中;历史 writeback 阶段维持原有的示例省略。 + +两个旧 CLI 参数写法现在暴露同一份共享完整契约;材料回读、产生效果前拒绝和 native +输入分别验证。已验证的 Native 外层 controller 输入没有内联结算 authoring owner,authoring 仍保留原文; +共享不会虚构该 owner 或转移结算权。四项旧 projection 测试仍期待完整候选尚未采用的多行标题或 Explore +指导共享,在前一分支 head 和本增量都失败。它们作为 C1 迁移缺口保留,不豁免门禁。 +本片不结清这些消费者,也不证明已安装 App 的采用。 + +固定的合成 authoring fixture 中,原未分阶段 CLI 为 22,062 个紧凑 UTF-8 JSON 字节, +融合契约为 23,266(+5.46%)。启用 capture 分阶段时,原 entry/review/writeback 合计 +24,311,融合为 23,349。原 CLI projection 模块冻结在 `895f38fc5`,输入及投影读取阶段 +保持一致。追加读取、请求包装、tokens/cache、延迟和模型成果尚未测量。共享消除了 +候选中的重复实现和重复指令,但不能证明优于 quota 已有共享或完整成本 diff 正向。 + 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 只有必要语义保留且同等验收成果的完整成本降低,或可比预算下验收成果改善,才判正向。 From 6619b6c132b220ef6b92591b4f0ef4028ce0ccca Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:20:29 +0800 Subject: [PATCH 18/33] test(quota): retain supported envelope before negative semantic reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_quota_acceptance_references.py | 3 ++- tests/control_plane/test_quota_authoring_references.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/control_plane/test_quota_acceptance_references.py b/tests/control_plane/test_quota_acceptance_references.py index f354624411..d5533dc7b7 100644 --- a/tests/control_plane/test_quota_acceptance_references.py +++ b/tests/control_plane/test_quota_acceptance_references.py @@ -179,6 +179,7 @@ def test_complete_readers_reject_dangling_or_cyclic_acceptance_even_with_valid_h from loopx.control_plane.quota.turn_envelope import _turn_envelope_result from loopx.control_plane.turn_driver.codex_cli import _prompt contract = compose_turn_decision(packet()) + envelope = build_turn_envelope(contract) summary = contract["agent_todo_summary"] if mutation == "dangling": summary["goal_acceptance_contract_ref"] = "$.interaction_contract.agent_channel.work_context.sources[9].content.goal_acceptance_contract" @@ -198,7 +199,7 @@ def test_complete_readers_reject_dangling_or_cyclic_acceptance_even_with_valid_h lambda: build_turn_envelope(contract), lambda: _turn_envelope_result("replan_view", payload=contract, source_hash=_canonical_digest(contract), stage="review"), - lambda: _prompt({"work_contract": contract}), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), ): with pytest.raises(RuntimeError, match="acceptance reference"): read() diff --git a/tests/control_plane/test_quota_authoring_references.py b/tests/control_plane/test_quota_authoring_references.py index 841f9a7eb6..007c4652cf 100644 --- a/tests/control_plane/test_quota_authoring_references.py +++ b/tests/control_plane/test_quota_authoring_references.py @@ -88,6 +88,7 @@ def test_invalid_authoring_reference_is_rejected_even_with_valid_content_hash(mu from loopx.control_plane.turn_driver.codex_cli import _prompt source, _, index = packet() contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) writeback = contract["replan_action_packet"]["writeback_contract"] writeback.pop("vision_authoring", None) reference = f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{index}].vision_authoring" @@ -106,7 +107,7 @@ def test_invalid_authoring_reference_is_rejected_even_with_valid_content_hash(mu writeback["vision_authoring_ref"] = {"path": reference} contract, _ = _resign_contract(contract) for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), - lambda: _prompt({"work_contract": contract}), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), lambda: _turn_envelope_result("replan_view", payload=contract, source_hash=_canonical_digest(contract), stage="writeback")): with pytest.raises(RuntimeError, match="authoring reference"): From c233b8ae6d15b6612a4b1fadac69e2b566f5468a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:36:43 +0800 Subject: [PATCH 19/33] refactor(quota): share Explore guidance through one composition owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/cli_projection.py | 37 +---- loopx/control_plane/quota/turn_envelope.ts | 63 +++++++- .../test_quota_cli_projection.py | 29 +++- .../test_quota_explore_guidance_references.py | 135 ++++++++++++++++++ .../control_plane/test_replan_staged_read.py | 11 +- 5 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 tests/control_plane/test_quota_explore_guidance_references.py diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 7262c75b88..096a236572 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -731,39 +731,10 @@ def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any] def _reference_explore_writeback_guidance(payload: dict[str, Any]) -> dict[str, Any]: - """Keep identical optional instructions once in this same CLI packet. - - This is transport-only reference projection, after all decisions. Preserve - standalone Explore reads, differing text and the complete settlement owner. - """ - contract = payload.get("interaction_contract", {}) - agent = contract.get("agent_channel", {}) - context = agent.get("work_context", {}) - sources = context.get("sources", []) - steps = contract.get("cli_channel", {}).get("settlement_plan", {}).get("ordered_steps", []) - for step_index, step in enumerate(steps): - if step.get("kind") != "durable_writeback": - continue - for attachment_index, attachment in enumerate(step.get("optional_attachments", [])): - guidance = attachment.get("guidance") - if attachment.get("capability_id") != "explore" or not isinstance(guidance, str) or not guidance: - continue - ref = (f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{step_index}]" - f".optional_attachments[{attachment_index}].guidance") - updated = list(sources) - for index, source in enumerate(sources): - content = source.get("content", {}) - graph = content.get("graph", {}) - if source.get("kind") != "explore_turn_context" or graph.get("result_writeback_guidance") != guidance: - continue - graph = dict(graph) - graph.pop("result_writeback_guidance") - graph["result_writeback_guidance_ref"] = ref - updated[index] = {**source, "content": {**content, "graph": graph}} - if updated != sources: - return {**payload, "interaction_contract": {**contract, "agent_channel": { - **agent, "work_context": {**context, "sources": updated}}}} - return payload + """Legacy display adapter; reuse the complete contract's exact-sharing owner.""" + from .turn_envelope import _turn_envelope_result + + return _turn_envelope_result("reference_explore_guidance", payload=payload) def compact_quota_should_run_cli_payload( diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index b3eccabbd0..2f42626bf6 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -347,6 +347,62 @@ function referenceReplanAuthoring(payload: JsonObject): JsonObject { ...contract, vision_authoring_ref: reference}}}; } +/** Same-capture references have one typed settlement owner. Historical readers + * expand before moving work context, so a full-packet path never dangles in a + * staged envelope. Explore's independent read and domain guidance stay intact. */ +function resolveExploreGuidance(payload: JsonObject, context: JsonObject): JsonObject { + if (!Array.isArray(context.sources)) return context; + const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; + return {...context, sources: context.sources.map(value => { + const source = object(value), content = object(source.content), graph = object(content.graph); + if (source.kind !== "explore_turn_context" || !Object.hasOwn(graph, "result_writeback_guidance_ref")) return value; + const reference = graph.result_writeback_guidance_ref; + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.cli_channel\.settlement_plan\.ordered_steps\[(0|[1-9]\d*)\]\.optional_attachments\[(0|[1-9]\d*)\]\.guidance$/.exec(reference) : null; + const step = object(match && Array.isArray(steps) ? steps[Number(match[1])] : null); + const attachment = object(match && Array.isArray(step.optional_attachments) + ? step.optional_attachments[Number(match[2])] : null); + if (Object.hasOwn(graph, "result_writeback_guidance") || step.kind !== "durable_writeback" || + attachment.capability_id !== "explore" || Object.hasOwn(attachment, "guidance_ref") || + typeof attachment.guidance !== "string" || !attachment.guidance) { + throw new EffectRuntimeRequestError("Explore guidance reference must resolve to one inline durable-writeback Explore attachment"); + } + delete graph.result_writeback_guidance_ref; + return {...source, content: {...content, graph: {...graph, result_writeback_guidance: attachment.guidance}}}; + })}; +} + +function referenceExploreGuidance(payload: JsonObject): JsonObject { + const interaction = object(payload.interaction_contract), agent = object(interaction.agent_channel); + const context = object(agent.work_context); + resolveExploreGuidance(payload, context); // Validate existing references, including idempotent reads. + if (!Array.isArray(context.sources)) return payload; + const steps = object(object(interaction.cli_channel).settlement_plan).ordered_steps; + if (!Array.isArray(steps)) return payload; + const owners = new Map(); + steps.forEach((value, stepIndex) => { + const step = object(value); + if (step.kind !== "durable_writeback" || !Array.isArray(step.optional_attachments)) return; + step.optional_attachments.forEach((value, attachmentIndex) => { + const attachment = object(value), guidance = attachment.guidance; + if (attachment.capability_id !== "explore" || Object.hasOwn(attachment, "guidance_ref") || + typeof guidance !== "string" || !guidance || owners.has(guidance)) return; + owners.set(guidance, `$.interaction_contract.cli_channel.settlement_plan.ordered_steps[${stepIndex}].optional_attachments[${attachmentIndex}].guidance`); + }); + }); + const sources = context.sources.map(value => { + const source = object(value), content = object(source.content), graph = object(content.graph); + const guidance = graph.result_writeback_guidance; + const reference = typeof guidance === "string" ? owners.get(guidance) : undefined; + if (source.kind !== "explore_turn_context" || typeof guidance !== "string" || !reference || + Buffer.byteLength(JSON.stringify(guidance), "utf8") <= Buffer.byteLength(reference, "utf8") + 8) return value; + delete graph.result_writeback_guidance; + return {...source, content: {...content, graph: {...graph, result_writeback_guidance_ref: reference}}}; + }); + return {...payload, interaction_contract: {...interaction, agent_channel: {...agent, + work_context: {...context, sources}}}}; +} + function compactFields( value: unknown, fields: readonly string[], @@ -487,6 +543,7 @@ function unboundExploreReplan(payload: JsonObject, plan: JsonObject): boolean { * Different revisions, other source fields and unavailable context stay intact. */ function replanWorkContext(payload: JsonObject, context: JsonObject): JsonObject { + context = resolveExploreGuidance(payload, context); const reads = replanReadCommands(payload); const core = object(object(object(payload.autonomous_replan_obligation).replan_context).core_goal); const acceptance = object(core.acceptance_contract); @@ -1298,6 +1355,7 @@ function requireCompleteDecisionSource(source: JsonObject): void { // object (including optional facts) rather than its normalized decode. decodeInteractionContract(source.interaction_contract); replanAuthoringContract(source); + resolveExploreGuidance(source, object(object(object(source.interaction_contract).agent_channel).work_context)); // This root field selects execution, unlike optional evidence carried intact // in the source. Reuse both registered owners; do not inherit the historical // adapter's unknown-action fallback at the new version boundary. @@ -1390,7 +1448,7 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { })}}}}; } } - source = referenceReplanAuthoring(referenceGoalAcceptance(source)); + source = referenceExploreGuidance(referenceReplanAuthoring(referenceGoalAcceptance(source))); requireCompleteDecisionSource(source); const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, @@ -1500,6 +1558,9 @@ function bindTurnDecisionCapture(request: JsonObject): JsonObject { export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; + if (operation === "reference_explore_guidance") { + return referenceExploreGuidance(requireJsonObject(request.payload, "Explore guidance sharing source")); + } if (operation === "reference_replan_authoring") { return referenceReplanAuthoring(requireJsonObject(request.payload, "authoring sharing source")); } diff --git a/tests/control_plane/test_quota_cli_projection.py b/tests/control_plane/test_quota_cli_projection.py index 6fe13f08f6..551e97235d 100644 --- a/tests/control_plane/test_quota_cli_projection.py +++ b/tests/control_plane/test_quota_cli_projection.py @@ -234,8 +234,10 @@ def test_real_cli_explore_reference_resolves_to_standalone_guidance(tmp_path, mo registry.write_text(json.dumps(config)) fixture._initialize_fixture_authority(project, runtime, provider=provider) binding = ("--goal-id", fixture.GOAL_ID, "--agent-id", fixture.AGENT_ID) + capture = tmp_path / "captured" rc, guard = fixture._run_cli(registry, runtime, "quota", "should-run", "--codex-app", - *binding, "--todo-id", fixture.TODO_ID, "--turn-instance-id", "guidance-read", cwd=project) + *binding, "--todo-id", fixture.TODO_ID, "--turn-instance-id", "guidance-read", + "--decision-output-dir", str(capture), cwd=project) assert rc == 0, guard sources = guard["interaction_contract"]["agent_channel"]["work_context"]["sources"] graph = next(source["content"]["graph"] for source in sources if source.get("kind") == "explore_turn_context") @@ -246,6 +248,31 @@ def test_real_cli_explore_reference_resolves_to_standalone_guidance(tmp_path, mo rc, standalone = fixture._run_cli(registry, runtime, "explore", "turn-context", *binding, cwd=project) assert rc == 0, standalone assert owner == standalone["graph"]["result_writeback_guidance"] + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from test_complete_turn_decision import _canonical_digest, _resign_contract + saved = json.loads((capture / "decision.json").read_text()) + assert saved == guard + envelope = build_turn_envelope(saved) + recovered = next(source["content"]["graph"] for source in envelope["work_context"]["sources"] + if source.get("kind") == "explore_turn_context") + assert recovered["result_writeback_guidance"] == owner + assert "result_writeback_guidance_ref" not in recovered + invalid = deepcopy(saved) + invalid_graph = next(source["content"]["graph"] for source in invalid["interaction_contract"][ + "agent_channel"]["work_context"]["sources"] if source.get("kind") == "explore_turn_context") + invalid_graph["result_writeback_guidance_ref"] = ( + "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[999].optional_attachments[0].guidance") + invalid, _ = _resign_contract(invalid) + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text(json.dumps(invalid)) + state = project / f".codex/goals/{fixture.GOAL_ID}/ACTIVE_GOAL_STATE.md" + before = state.read_bytes() + rc, refused = fixture._run_cli(registry, runtime, "quota", "replan-view", + "--decision-file", str(invalid_path), "--source-hash", _canonical_digest(invalid), + "--replan-stage", "writeback", cwd=project) + assert rc != 0 + assert "Explore guidance reference" in json.dumps(refused) + assert state.read_bytes() == before assert fixture._spend_run_count(runtime) == 0 diff --git a/tests/control_plane/test_quota_explore_guidance_references.py b/tests/control_plane/test_quota_explore_guidance_references.py new file mode 100644 index 0000000000..78e9de9b96 --- /dev/null +++ b/tests/control_plane/test_quota_explore_guidance_references.py @@ -0,0 +1,135 @@ +"""Explore instructions survive sharing, historical views and captured recovery.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import _reference_explore_writeback_guidance +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, build_turn_envelope, compose_turn_decision, + prepare_context_delivery, quota_action_signature_document, restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + guidance = "Keep original requirements, lease ownership, negative evidence and stop conditions. " * 20 + source["interaction_contract"]["agent_channel"]["work_context"] = {"complete": True, "sources": [{ + "kind": "explore_turn_context", "source": "turn_start_capability_hook", + "hook_id": "explore.turn_context", "capability_id": "explore", + "content": {"graph": {"result_writeback_guidance": guidance, + "writeback_results": [{"status": "refuted", "evidence": "fixture:negative"}]}}}]} + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}, "ordered_steps": [ + {"kind": "validation"}, {"kind": "durable_writeback", "optional_attachments": [ + {"capability_id": "explore", "guidance": guidance, "required": False}]}]} + return source, guidance + + +def graph(source): + return source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["graph"] + + +def attachment(source): + return source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["optional_attachments"][0] + + +@pytest.fixture(params=[_reference_explore_writeback_guidance, compose_turn_decision], ids=["legacy-display", "complete"]) +def share(request): + return request.param + + +@pytest.mark.parametrize("captured", [False, True]) +def test_shared_guidance_preserves_material_and_settlement_across_readers(share, captured): + source, guidance = packet() + if captured: + source["replan_read_path"] = "/synthetic/decision.json" + original = deepcopy(source) + shared = share(source) + assert source == original + assert share(shared) == shared + assert graph(shared) == {"writeback_results": graph(source)["writeback_results"], + "result_writeback_guidance_ref": "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[1].optional_attachments[0].guidance"} + assert shared["interaction_contract"]["cli_channel"] == source["interaction_contract"]["cli_channel"] + if share is _reference_explore_writeback_guidance and not captured: + assert quota_action_signature_document(shared) == quota_action_signature_document(source) + envelope = build_turn_envelope(shared) + material = envelope["work_context"]["sources"][0]["content"]["graph"] + assert material == graph(source) + assert material["result_writeback_guidance"] == guidance + assert envelope["action_signature"]["matches"] is True + if captured: + projected = _turn_envelope_result("replan_cli_view", payload=shared) + assert graph(projected) == graph(source) + writer = _turn_envelope_result("replan_view", payload=shared, + source_hash=_canonical_digest(shared), stage="writeback") + assert writer["writeback"]["settlement_plan"]["ordered_steps"][1]["optional_attachments"][0] == attachment(source) + + +@pytest.mark.parametrize("mutation", ["different", "wrong-step", "wrong-capability", "missing-owner", "short", "wrong-source"]) +def test_nonidentical_or_unavailable_guidance_stays_inline(share, mutation): + source, _ = packet() + if mutation == "different": + attachment(source)["guidance"] += " Extra stop." + elif mutation == "wrong-step": + source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["kind"] = "validation" + elif mutation == "wrong-capability": + attachment(source)["capability_id"] = "another" + elif mutation == "missing-owner": + attachment(source).pop("guidance") + elif mutation == "short": + attachment(source)["guidance"] = graph(source)["result_writeback_guidance"] = "Keep evidence." + else: + source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["kind"] = "other_context" + assert graph(share(source)) == graph(source) + + +@pytest.mark.parametrize("mutation", ["dangling", "wrong-step", "wrong-capability", "chain", "both", "malformed"]) +def test_invalid_guidance_reference_is_refused_despite_valid_integrity(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + source, _ = packet() + contract = compose_turn_decision(source) + envelope = build_turn_envelope(contract) + if mutation == "dangling": + graph(contract)["result_writeback_guidance_ref"] = graph(contract)["result_writeback_guidance_ref"].replace("[1]", "[99]") + elif mutation == "wrong-step": + contract["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["kind"] = "validation" + elif mutation == "wrong-capability": + attachment(contract)["capability_id"] = "another" + elif mutation == "chain": + attachment(contract)["guidance_ref"] = "$.unknown" + elif mutation == "both": + graph(contract)["result_writeback_guidance"] = "Conflicting instruction." + else: + graph(contract)["result_writeback_guidance_ref"] = {"path": "$.unknown"} + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="writeback")): + with pytest.raises(RuntimeError, match="Explore guidance reference"): + read() + + +@pytest.mark.parametrize("change_guidance, complete", [(False, True), (True, True), (True, False)]) +def test_guidance_restores_after_delta_or_full_fallback(change_guidance, complete): + source, guidance = packet() + source["interaction_contract"]["agent_channel"]["work_context"]["complete"] = complete + before = compose_turn_decision(source) + if change_guidance: + guidance += " New authorization boundary." + graph(source)["result_writeback_guidance"] = attachment(source)["guidance"] = guidance + else: + source["replan_action_packet"]["uncovered_frontier"] = ["A new observed gap"] + after = compose_turn_decision(source) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(before, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == ("delta" if complete else "full") + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == after + assert build_turn_envelope(restored)["work_context"]["sources"][0]["content"]["graph"]["result_writeback_guidance"] == guidance diff --git a/tests/control_plane/test_replan_staged_read.py b/tests/control_plane/test_replan_staged_read.py index 238d19f820..396afb2ccd 100644 --- a/tests/control_plane/test_replan_staged_read.py +++ b/tests/control_plane/test_replan_staged_read.py @@ -398,6 +398,12 @@ def test_real_guard_capture_read_and_writeback_without_another_guard(tmp_path, m step = next(row for row in changed["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] if row["kind"] == "durable_writeback") attachment = next(row for row in step["optional_attachments"] if row["capability_id"] == "explore") + # Reconstruct the historical inline fixture before mutating it. + # The complete producer now shares this exact same-capture owner. + assert graph.pop("result_writeback_guidance_ref") == ( + f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{changed['interaction_contract']['cli_channel']['settlement_plan']['ordered_steps'].index(step)}]" + f".optional_attachments[{step['optional_attachments'].index(attachment)}].guidance") + graph["result_writeback_guidance"] = attachment["guidance"] if mutation == "custom": graph["result_writeback_guidance"] += " Stop before a new operation." attachment["guidance"] = graph["result_writeback_guidance"] @@ -438,7 +444,10 @@ def read(stage, expected_code=0): if row["kind"] == "durable_writeback") attachment = next(row for row in writer_step["optional_attachments"] if row["capability_id"] == "explore") - assert attachment["guidance"] == original_graph["result_writeback_guidance"] + assert "result_writeback_guidance" not in original_graph + assert original_graph["result_writeback_guidance_ref"] == ( + f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{writer['writeback']['settlement_plan']['ordered_steps'].index(writer_step)}]" + f".optional_attachments[{writer_step['optional_attachments'].index(attachment)}].guidance") assert attachment["attachment_template"] == original_graph["result_attachment_template"] assert writer["writeback"]["settlement_plan"] == source["interaction_contract"]["cli_channel"]["settlement_plan"] assert writer["settlement_command"].endswith(" --settle-turn") From f6844f87525f76f17a9a6d581b1276a9c064398d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:36:49 +0800 Subject: [PATCH 20/33] docs(rfc): record shared guidance integration and cost limits Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 41 ++++++++++++++++--- ...ller-executor-unified-contract-v0.zh-CN.md | 32 ++++++++++++--- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index f559dc9aef..3a239b66b5 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -708,8 +708,8 @@ non-staged CLI (+5.37%). With capture staging, fused bytes are 23,515 versus reads. The original display-sharing helper is frozen at `895f38fc5` for this comparison. The acceptance-free workload above remains unchanged. This absorbs an existing quota efficiency into the common path; it does not establish an -advantage over quota or full-cost benefit. Next qualify the remaining vision -and guidance sharing in their existing owners before deferred-detail choices. Keep cold and staged totals separate and retain negative results; do +advantage over quota or full-cost benefit. The subsequent guidance increment below narrows that gap; next qualify the remaining +vision/title sharing in their existing owners before deferred-detail choices. Keep cold and staged totals separate and retain negative results; do not force a byte target by dropping unique instructions. The next #7430 increment shares replan vision-authoring instructions through @@ -728,10 +728,11 @@ readback, rejection before effects and native input are qualified separately. The exercised native outer-controller inputs lack an inline settlement authoring owner, so their authoring remains inline; sharing never invents that owner or transfers settlement authority. -Four legacy projection tests still expect multiline-title or Explore-guidance -sharing absent from the complete candidate, and fail at both the prior branch -head and this increment. They remain visible C1 migration gaps, not waived gates. -This slice does not settle those consumers or qualify installed App adoption. +At the authoring checkpoint, four legacy projection tests expected multiline-title +or Explore-guidance sharing absent from the complete candidate and failed at both +the preceding and authoring heads. The guidance increment below resolves two; +the two multiline-title cases remain visible C1 migration gaps, not waived gates. +These slices do not qualify installed App adoption. For the fixed synthetic authoring fixture, the original non-staged CLI is 22,062 compact UTF-8 JSON bytes and the fused contract is 23,266 (+5.46%). With capture @@ -742,6 +743,34 @@ latency and model outcomes remain unmeasured. Sharing eliminates a duplicate implementation and duplicate instructions in the candidate, but does not prove an improvement over quota's existing sharing or a positive full-cost diff. + +The Explore guidance increment removes the Python exact-match loop and reuses the +same TypeScript composition boundary for complete and legacy display callers. +Only identical instructions with an existing durable-writeback Explore attachment +share that same-capture owner when the reference is smaller. Unique/short text and +missing owners stay inline. Invalid, conflicting or chained references reject; +no instruction is shortened and an optional attachment gains no new obligation. +Historical staged and unstaged readers resolve the reference before moving context, +then reuse Explore's existing domain projection. Independent Explore reads still +return full guidance. The compatibility helper retires with the legacy display +callers; it owns no separate decision or sharing algorithm. + +Real isolated File/SQLite CLI captures now satisfy the two previously failing +Explore-sharing cases, including standalone material comparison, historical +readback and invalid-capture refusal without state/spend changes. Existing staged +CLI tests still exercise unavailable-registry reads, tamper refusal and actual +writeback/settlement. Delta reconstruction preserves changed instructions and +lost-baseline recovery; incomplete context falls back to full delivery. These are +transport and local integration results, not installed App or model-outcome proof. + +With the same original projection frozen at `895f38fc5`, the synthetic guidance +fixture uses 22,848 compact UTF-8 JSON bytes on the original non-staged path and +24,320 fused (+6.44%). Captured original entry/review/writeback total 26,081 versus +24,403 fused bytes. The guidance-free fixture remains 20,543 versus 21,738 bytes +non-staged. Required extra reads, framing, tokens/cache, latency and accepted model +outcomes remain unmeasured. Shared code and restored quota information efficiency +do not establish a positive complete-cost diff; C1 remains open. + Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, latency, failures/retries, verification and human intervention. A smaller first diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index f6cc41b181..3dc82d050b 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -533,8 +533,8 @@ composition 调用内部完成共享。未声称延迟改善。 原未分阶段 CLI 为 22,237(+5.37%)。启用 capture 分阶段时,融合契约为 23,515, 原 entry/review/writeback 合计为 24,509,未计追加材料读取。对照冻结 `895f38fc5` 的原展示共享 helper;上述不含验收副本的 workload 数字不变。这把 quota 已有的效率 -吸收到共同路径,不能证明优于 quota 或完整成本获益。下一步先在既有 owner 验证剩余 -vision 和 guidance 共享,再评估延后详情。冷启动与分阶段总量分别保留, +吸收到共同路径,不能证明优于 quota 或完整成本获益。下述 guidance 增量进一步缩小缺口;下一步先在既有 owner 验证剩余 +vision/title 共享,再评估延后详情。冷启动与分阶段总量分别保留, 保留负结果,不能靠删除独有指令达到字节目标。 #7430 后续增量通过同一个 TypeScript composition 边界共享 replan 的 vision-authoring @@ -546,9 +546,10 @@ vision 和 guidance 共享,再评估延后详情。冷启动与分阶段总量 两个旧 CLI 参数写法现在暴露同一份共享完整契约;材料回读、产生效果前拒绝和 native 输入分别验证。已验证的 Native 外层 controller 输入没有内联结算 authoring owner,authoring 仍保留原文; -共享不会虚构该 owner 或转移结算权。四项旧 projection 测试仍期待完整候选尚未采用的多行标题或 Explore -指导共享,在前一分支 head 和本增量都失败。它们作为 C1 迁移缺口保留,不豁免门禁。 -本片不结清这些消费者,也不证明已安装 App 的采用。 +共享不会虚构该 owner 或转移结算权。authoring 检查点的四项旧 projection 测试期待 +完整候选尚未采用的多行标题或 Explore 指导共享,在此前及 authoring head 都失败。 +下述 guidance 增量解决其中两项;两项多行标题用例仍是 C1 迁移缺口,不豁免门禁。 +这些切片不证明已安装 App 的采用。 固定的合成 authoring fixture 中,原未分阶段 CLI 为 22,062 个紧凑 UTF-8 JSON 字节, 融合契约为 23,266(+5.46%)。启用 capture 分阶段时,原 entry/review/writeback 合计 @@ -556,6 +557,27 @@ vision 和 guidance 共享,再评估延后详情。冷启动与分阶段总量 保持一致。追加读取、请求包装、tokens/cache、延迟和模型成果尚未测量。共享消除了 候选中的重复实现和重复指令,但不能证明优于 quota 已有共享或完整成本 diff 正向。 + +Explore guidance 增量删除 Python 的精确匹配循环,完整契约与旧展示调用者复用同一个 +TypeScript composition 边界。仅当指令完全一致、已有 durable-writeback Explore +附件 owner 且引用更小时,才共享同一 capture 的 owner。独有/短文本及缺失 owner +保留原文。非法、冲突或成链引用拒绝;不缩短指令,也不给可选附件新增义务。 +历史分阶段及未分阶段 reader 在移动上下文前解析引用,再复用 Explore 既有领域投影。 +独立 Explore 读取仍返回完整指导。兼容 helper 随旧展示调用者退役,不拥有独立决策或共享算法。 + +真实隔离 File/SQLite CLI capture 现在通过此前失败的两项 Explore 共享用例, +包括独立材料对照、历史回读及非法 capture 在状态/额度不变时拒绝。 +既有分阶段 CLI 测试仍执行 registry 不可用时读取、篡改拒绝及实际写回/结算。 +差分重建保留变化后的指令并支持 baseline 丢失恢复;不完整上下文回退完整交付。 +这些证明传输及本地集成,不证明已安装 App 或模型成果。 + +同样冻结 `895f38fc5` 原投影,合成 guidance fixture 的原未分阶段路径为 22,848 个 +紧凑 UTF-8 JSON 字节,融合为 24,320(+6.44%)。启用 capture 后,原 +entry/review/writeback 合计 26,081,融合为 24,403。不含 guidance 的 fixture +未分阶段仍为 20,543 对 21,738 字节。必需追加读取、请求包装、tokens/cache、延迟及 +模型验收成果尚未测量。代码共享及恢复 quota 已有的信息效率不能证明完整成本 diff 正向; +C1 继续保持未完成。 + 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 只有必要语义保留且同等验收成果的完整成本降低,或可比预算下验收成果改善,才判正向。 From e418ebd6c3c12f6cc6b5c64579e0ef067ace32b3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:42:53 +0800 Subject: [PATCH 21/33] test(turn): qualify complete capture and settlement consumers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_turn_envelope_capability_facts.py | 38 +++++++++++++++---- .../test_turn_envelope_settlement.py | 3 +- .../test_turn_host_decision_capture.py | 14 +++++-- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/control_plane/test_turn_envelope_capability_facts.py b/tests/control_plane/test_turn_envelope_capability_facts.py index 107ccf0a0f..2dfa922054 100644 --- a/tests/control_plane/test_turn_envelope_capability_facts.py +++ b/tests/control_plane/test_turn_envelope_capability_facts.py @@ -12,6 +12,7 @@ import pytest from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from loopx.control_plane.todos.active_state_todo_parser import parse_active_state_todos from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan from tests.control_plane.canonical_authority_fixture import ( initialize_canonical_authority, isolate_sqlite_runtime, @@ -34,10 +35,13 @@ def test_real_captured_guard_preserves_required_and_missing_capabilities( state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state.write_text(state.read_text().replace("action_kind=validate ", "action_kind=validate continuation_policy=same_agent_non_delivery ")) - code, listed = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) - assert code == 0, listed + # Initialize only this disposable fixture; ordinary reads must refuse + # unregistered projections instead of restoring canonical work from them. + code, refused = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) + assert code == 1 and refused["error_code"] == "canonical_authority_required" + listed = parse_active_state_todos(state.read_text(), state_path=state, item_limit=None) projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode="soft_claim", todos=listed["todos"], + goal_id=GOAL_ID, handoff_mode="soft_claim", todos=listed["agent_todos"]["items"], ) initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) if mode != "off": @@ -54,6 +58,14 @@ def test_real_captured_guard_preserves_required_and_missing_capabilities( "--turn-envelope", "--decision-output-dir", str(capture), cwd=project) assert code == (0 if network_available else 1), envelope full = json.loads((capture / "decision.json").read_text()) + # Admission failures retain their typed diagnostic/historical projection; + # successful admission emits the complete contract through either spelling. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + if network_available: + assert envelope == full and full["schema_version"] == "loopx_turn_envelope_v1" + envelope = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json")) + else: + assert full.get("schema_version") != "loopx_turn_envelope_v1" gate = full["capability_gate"] assert "network" in gate["required"] assert ("network" in gate["missing"]) is (not network_available) @@ -88,10 +100,13 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" state.write_text(state.read_text().replace("action_kind=validate ", "action_kind=validate continuation_policy=same_agent_non_delivery ")) - code, listed = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) - assert code == 0, listed + # Initialize only this disposable fixture; ordinary reads must refuse + # unregistered projections instead of restoring canonical work from them. + code, refused = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) + assert code == 1 and refused["error_code"] == "canonical_authority_required" + listed = parse_active_state_todos(state.read_text(), state_path=state, item_limit=None) initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode="soft_claim", todos=listed["todos"], + goal_id=GOAL_ID, handoff_mode="soft_claim", todos=listed["agent_todos"]["items"], ), state_path=state, provider=provider) # A synthetic retained checkpoint drives the real frontier and CLI owners. @@ -126,6 +141,11 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( "--decision-output-dir", str(capture), cwd=project) assert code == 0, envelope full = json.loads((capture / "decision.json").read_text()) + assert envelope == full and full["schema_version"] == "loopx_turn_envelope_v1" + # The real CLI now emits the complete contract. Retain the historical + # decoder and its independent signature/refusal checks below. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json")) audit = full["vision_continuation_audit"] assert audit["required"] and audit["required_before_closeout"] assert audit["vision_gap_judge"]["done"] is False @@ -185,7 +205,8 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( plan = build_loopx_turn_plan(envelope, host="dsh", execution_mode="isolated-headless", turn_instance_id="vision-contract-turn") assert plan["route"]["would_invoke_host"] is True - assert envelope["action"]["delivery_allowed"] is True + # A replan route permits planning, not ordinary delivery. + assert envelope["action"]["delivery_allowed"] is (not replan) request = {"schema_version": "loopx_turn_host_request_v0", "turn_key": plan["transaction"]["turn_key"], "route": "replan_required" if replan else "primary_delivery", "turn_envelope": envelope} result = subprocess.run(command, input=json.dumps(request), text=True, capture_output=True, timeout=30) @@ -226,7 +247,8 @@ def test_captured_vision_contract_preserves_closeout_and_original_observation( read = subprocess.run(shlex.split(detail["full_decision"]), capture_output=True, check=True).stdout assert read == original assert build_turn_envelope(json.loads(read))["action_signature"]["source_decision_hash"] == authority["action_signature"]["source_decision_hash"] - assert "writeback" not in authority and "scheduler" not in authority + assert "writeback" not in authority + assert authority["scheduler"] == envelope["scheduler"] assert "Do not execute its state or scheduler commands" in prompt for field, value in ( ("required_before_closeout", list(reversed(projected["required_before_closeout"]))), diff --git a/tests/control_plane/test_turn_envelope_settlement.py b/tests/control_plane/test_turn_envelope_settlement.py index 904293e690..351b8b44b9 100644 --- a/tests/control_plane/test_turn_envelope_settlement.py +++ b/tests/control_plane/test_turn_envelope_settlement.py @@ -102,7 +102,8 @@ def test_generated_json_commands_settle_original_turn_once(tmp_path, profile_arg assert rc == 0, full rc, envelope = _run_cli(registry, runtime, *args, "--turn-envelope") assert rc == 0, envelope - plan = envelope["writeback"]["settlement_plan"] + assert envelope["schema_version"] == full["schema_version"] == "loopx_turn_envelope_v1" + plan = envelope["interaction_contract"]["cli_channel"]["settlement_plan"] assert plan == full["interaction_contract"]["cli_channel"]["settlement_plan"] assert plan["identity"] == full["heartbeat_receipt"]["settlement_identity"] assert [step["kind"] for step in plan["ordered_steps"]] == [ diff --git a/tests/control_plane/test_turn_host_decision_capture.py b/tests/control_plane/test_turn_host_decision_capture.py index f0d87b05f5..6bd5efbfe8 100644 --- a/tests/control_plane/test_turn_host_decision_capture.py +++ b/tests/control_plane/test_turn_host_decision_capture.py @@ -75,7 +75,8 @@ def test_real_host_reads_original_capture_after_source_change( else: assert code == 0 and result["status"] == "committed", result request = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) - detail = request["turn_envelope"]["detail_ref"] + assert "turn_envelope" not in request + detail = request["detail_ref"] capture = detail["captured_decision"] assert capture["turn_instance_id"] == "original-observation" assert capture["turn_key"] == request["turn_key"] @@ -90,13 +91,14 @@ def test_real_host_reads_original_capture_after_source_change( else: assert original.endswith(b"\n") and b"\n" not in original[:-1] source_a = json.loads(original) + assert request["work_contract"] == source_a assert source_a["goal_id"] == f.GOAL_ID assert source_a["agent_identity"]["agent_id"] == f.AGENT_ID # Direct `turn` bypasses quota CLI emission. Check its actual subprocess # prompt against the same original contracts, including recovery below. for field in ("work_lane_contract", "execution_obligation", "automation_liveness"): assert source_a[field] - assert request["turn_envelope"]["contract_capsule"][field] == source_a[field] + assert request["work_contract"][field] == source_a[field] # Change the real provider through its native CAS CLI, not the projection. prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] code, current = f._run_cli([*prefix, "todo", "list", "--goal-id", f.GOAL_ID, "--todo-id", f.TODO_ID]) @@ -116,7 +118,10 @@ def test_real_host_reads_original_capture_after_source_change( journal_path = turn_journal_path(runtime, goal_id=f.GOAL_ID, turn_key=request["turn_key"]) rebound = prepare_turn_decision_capture(plan, journal_path=journal_path, decision=None, existing_journal=True) - assert rebound["turn_envelope"]["detail_ref"] == detail + from loopx.control_plane.turn_driver.host_candidate import complete_host_input + rebound_input = complete_host_input(rebound) + assert rebound_input["detail_ref"] == detail + assert rebound_input["work_contract"] == source_a replay = list(base) index = replay.index("--turn-instance-id") del replay[index:index + 2] @@ -137,7 +142,8 @@ def test_real_host_reads_original_capture_after_source_change( "--retry-failed-turn", "--execute"]) assert code == 0 and recovered["status"] == "committed", json.dumps(recovered) retried = json.loads((workspace / "received.txt").read_text().split("Turn request:\n", 1)[1]) - assert retried["turn_envelope"] == request["turn_envelope"] + assert retried["work_contract"] == request["work_contract"] + assert retried["detail_ref"] == detail assert path.read_bytes() == original code, repeated = f._run_cli([*replay, "--resume-turn-key", request["turn_key"], "--execute"]) assert code == 0, repeated From 411707c067e70493c08ce23079af6e2eedb03e96 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:42:53 +0800 Subject: [PATCH 22/33] docs(rfc): retain rejected-admission migration boundary Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/controller-executor-unified-contract-v0.md | 11 +++++++++++ .../controller-executor-unified-contract-v0.zh-CN.md | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 3a239b66b5..eecb5f77f2 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -763,6 +763,17 @@ writeback/settlement. Delta reconstruction preserves changed instructions and lost-baseline recovery; incomplete context falls back to full delivery. These are transport and local integration results, not installed App or model-outcome proof. + +Broader caller qualification also migrates old test assumptions to the admitted +complete contract: native subprocess capture/retry compares the entire original +contract, and generated settlement commands still settle only the original Turn. +Historical capability readers retain explicit canonical-fixture initialization, +refusal facts and scheduler information without granting execution authority. +Rejected capability admissions still emit their typed diagnostic/historical +projection, whereas admitted calls emit the complete contract. This remaining +failure-path migration belongs to C1 before full cutover; it is not a permanent +second contract. Synthetic native/stdio providers do not qualify model benefit. + With the same original projection frozen at `895f38fc5`, the synthetic guidance fixture uses 22,848 compact UTF-8 JSON bytes on the original non-staged path and 24,320 fused (+6.44%). Captured original entry/review/writeback total 26,081 versus diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 3dc82d050b..2d7d3a6054 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -571,6 +571,14 @@ TypeScript composition 边界。仅当指令完全一致、已有 durable-writeb 差分重建保留变化后的指令并支持 baseline 丢失恢复;不完整上下文回退完整交付。 这些证明传输及本地集成,不证明已安装 App 或模型成果。 + +扩大调用者验证也将旧测试假设迁移到准入后的完整契约:native 子进程 capture/重试 +比较整份原始契约,生成的结算命令仍仅结算原 Turn。历史 capability reader 维持 +显式初始化隔离 canonical fixture、拒绝事实及 scheduler 信息,不因此授予执行权。 +capability 准入被拒时仍交付 typed diagnostic/历史投影,准入成功才交付完整契约。 +这个失败路径迁移仍属于 C1 切换前缺口,不是永久第二套契约。 +合成 native/stdio provider 不证明模型收益。 + 同样冻结 `895f38fc5` 原投影,合成 guidance fixture 的原未分阶段路径为 22,848 个 紧凑 UTF-8 JSON 字节,融合为 24,320(+6.44%)。启用 capture 后,原 entry/review/writeback 合计 26,081,融合为 24,403。不含 guidance 的 fixture From 38a6333694af2b4a196500fc398f63996a6267d8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:05:49 +0800 Subject: [PATCH 23/33] refactor(quota): share verified task titles through one composition owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/cli_projection.py | 49 +------ loopx/control_plane/quota/turn_envelope.ts | 59 ++++++++- .../test_quota_cli_projection.py | 46 ++++++- .../test_quota_task_title_references.py | 122 ++++++++++++++++++ 4 files changed, 230 insertions(+), 46 deletions(-) create mode 100644 tests/control_plane/test_quota_task_title_references.py diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 096a236572..4de759a0c5 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json from typing import Any from .decision_summary import compact_quota_decision @@ -571,47 +570,13 @@ def _deduplicate_agent_lane_next_action( compact.pop("title", None) return compact - # The typed work-context owner has already verified the current full Task. - # A bounded lane label may differ from its full title. Reference only an - # exact duplicate in this same packet, never a summary or a prior capture. - contract = payload.get("interaction_contract") - channel = contract.get("agent_channel") if isinstance(contract, dict) else None - context = channel.get("work_context") if isinstance(channel, dict) else None - selected = payload.get("selected_todo") - if not isinstance(context, dict) or not isinstance(selected, dict): - return item - if (context.get("complete") is not True or context.get("selected_todo_text_verified") is not True - or not isinstance(item.get("todo_id"), str) or not item["todo_id"] or "title_ref" in item - or selected.get("todo_id") != item["todo_id"]): - return item - sources = context.get("sources", []) - if not isinstance(sources, list): - return item - matches = [(index, source.get("content", {})) for index, source in enumerate(sources) - if isinstance(source, dict) and source.get("source") == "selected_todo"] - if len(matches) != 1: - return item - index, content = matches[0] - if not isinstance(content, dict): - return item - todo = content.get("todo", {}) - if (not isinstance(todo, dict) or not isinstance(todo.get("text"), str) - or content.get("ok") is not True or content.get("matched") is not True - or content.get("ambiguous") is True or content.get("read_complete") is False - or todo.get("archive_state") == "archive" or todo.get("status") == "done" - or todo.get("todo_id") != item["todo_id"]): - return item - for field in ("text", "title"): - if todo.get(field) != title: - continue - ref = f"$.interaction_contract.agent_channel.work_context.sources[{index}].content.todo.{field}" - if len(json.dumps(title, ensure_ascii=False)) <= len(ref) + 8: - return item - compact = dict(item) - compact.pop("title") - compact["title_ref"] = ref - return compact - return item + from .turn_envelope import _turn_envelope_result + + # Exact source sharing has one owner with complete decision composition. + # The prefix-based display-only omission above is not a contract rule. + shared = _turn_envelope_result("reference_current_task_title", + payload={**payload, "agent_lane_next_action": item}) + return shared["agent_lane_next_action"] def _compact_goal_route_hint(route: dict[str, Any]) -> dict[str, Any]: diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 2f42626bf6..b7854c721e 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -403,6 +403,56 @@ function referenceExploreGuidance(payload: JsonObject): JsonObject { work_context: {...context, sources}}}}; } +/** The verified current Task is the only owner of a shared lane title. Keep + * the original declaration inline and require exact identity, never a label or + * prefix comparison. The legacy display and complete writer use this owner. */ +function currentTaskTitleOwner(payload: JsonObject): {index: number; todo: JsonObject} | null { + const lane = object(payload.agent_lane_next_action), selected = object(payload.selected_todo); + const context = object(object(object(payload.interaction_contract).agent_channel).work_context); + if (context.complete !== true || context.selected_todo_text_verified !== true || + typeof lane.todo_id !== "string" || !lane.todo_id || selected.todo_id !== lane.todo_id || + !Array.isArray(context.sources)) return null; + const matches = context.sources.map((value, index) => ({source: object(value), index})) + .filter(({source}) => source.source === "selected_todo"); + if (matches.length !== 1) return null; + const {source, index} = matches[0], content = object(source.content), todo = object(content.todo); + if (content.ok !== true || content.matched !== true || content.ambiguous === true || + content.read_complete === false || typeof todo.text !== "string" || + todo.archive_state === "archive" || todo.status === "done" || todo.todo_id !== lane.todo_id) return null; + return {index, todo}; +} + +function requireCurrentTaskTitleReference(payload: JsonObject): void { + const lane = object(payload.agent_lane_next_action); + if (!Object.hasOwn(lane, "title_ref")) return; + const reference = lane.title_ref; + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.agent_channel\.work_context\.sources\[(0|[1-9]\d*)\]\.content\.todo\.(text|title)$/.exec(reference) : null; + const owner = currentTaskTitleOwner(payload); + if (Object.hasOwn(lane, "title") || !match || !owner || owner.index !== Number(match[1]) || + typeof owner.todo[match[2]] !== "string" || !owner.todo[match[2]] || + Object.hasOwn(owner.todo, `${match[2]}_ref`)) { + throw new EffectRuntimeRequestError("Current Task title reference must resolve to the complete verified selected Task"); + } +} + +function referenceCurrentTaskTitle(payload: JsonObject): JsonObject { + const lane = object(payload.agent_lane_next_action), title = lane.title; + // Preserve historical caller-owned references; complete readers validate them + // separately. Display-only omission of a title already in a label stays local. + if (typeof title !== "string" || Object.hasOwn(lane, "title_ref")) return payload; + const owner = currentTaskTitleOwner(payload); + if (!owner) return payload; + for (const field of ["text", "title"]) { + if (owner.todo[field] !== title || Object.hasOwn(owner.todo, `${field}_ref`)) continue; + const reference = `$.interaction_contract.agent_channel.work_context.sources[${owner.index}].content.todo.${field}`; + if (Array.from(JSON.stringify(title)).length <= reference.length + 8) return payload; + delete lane.title; + return {...payload, agent_lane_next_action: {...lane, title_ref: reference}}; + } + return payload; +} + function compactFields( value: unknown, fields: readonly string[], @@ -1355,6 +1405,7 @@ function requireCompleteDecisionSource(source: JsonObject): void { // object (including optional facts) rather than its normalized decode. decodeInteractionContract(source.interaction_contract); replanAuthoringContract(source); + requireCurrentTaskTitleReference(source); resolveExploreGuidance(source, object(object(object(source.interaction_contract).agent_channel).work_context)); // This root field selects execution, unlike optional evidence carried intact // in the source. Reuse both registered owners; do not inherit the historical @@ -1448,7 +1499,10 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { })}}}}; } } - source = referenceExploreGuidance(referenceReplanAuthoring(referenceGoalAcceptance(source))); + source = referenceGoalAcceptance(source); + source = referenceReplanAuthoring(source); + source = referenceExploreGuidance(source); + source = referenceCurrentTaskTitle(source); requireCompleteDecisionSource(source); const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, @@ -1558,6 +1612,9 @@ function bindTurnDecisionCapture(request: JsonObject): JsonObject { export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; + if (operation === "reference_current_task_title") { + return referenceCurrentTaskTitle(requireJsonObject(request.payload, "current Task title sharing source")); + } if (operation === "reference_explore_guidance") { return referenceExploreGuidance(requireJsonObject(request.payload, "Explore guidance sharing source")); } diff --git a/tests/control_plane/test_quota_cli_projection.py b/tests/control_plane/test_quota_cli_projection.py index 551e97235d..710909aabc 100644 --- a/tests/control_plane/test_quota_cli_projection.py +++ b/tests/control_plane/test_quota_cli_projection.py @@ -19,10 +19,12 @@ @pytest.mark.parametrize("variant", ["text", "title", "different", "short", "incomplete", "unverified", "unmatched", "ambiguous", "partial", "failed", "wrong-id", "wrong-selection", "duplicate", "existing-ref", - "archived", "done", "boolean-id"]) + "archived", "done", "boolean-id", "short-unicode"]) def test_lane_title_reuses_only_exact_complete_current_task_in_same_packet(variant): body = "Preserve every requirement and source. 完整要求🙂\n" * 100 + "Stop before publication." title = body if variant != "short" else "Short label" + if variant == "short-unicode": + title = body = "完整🙂" * 20 todo = {"todo_id": "todo_current", "text": body, "status": "open"} if variant == "title": todo.update(text="[P1] " + body, title=body) @@ -78,9 +80,23 @@ def test_lane_title_reuses_only_exact_complete_current_task_in_same_packet(varia else: assert result == lane + from loopx.control_plane.quota.turn_envelope import compose_turn_decision + from test_replan_host_context_projection import _quota_payload + complete_source = _quota_payload() + complete_source.update(selected_todo=payload["selected_todo"], agent_lane_next_action=lane) + complete_source["interaction_contract"]["agent_channel"]["work_context"] = context + if variant == "existing-ref": + with pytest.raises(RuntimeError, match="Current Task title reference"): + compose_turn_decision(complete_source) + else: + complete = compose_turn_decision(complete_source) + assert complete["agent_lane_next_action"] == result + assert complete["interaction_contract"] == complete_source["interaction_contract"] + @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("captured", [False, True]) +def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_path, monkeypatch, provider, captured): from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _run_cli, _write_fixture @@ -96,8 +112,11 @@ def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_pa build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim"), state_path=state, provider=provider) original_state = state.read_bytes() + capture = tmp_path / "captured" + capture_args = ("--codex-app", "--turn-instance-id", "task-title-read", + "--decision-output-dir", str(capture)) if captured else () code, full = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, - "--agent-id", AGENT_ID, cwd=project) + "--agent-id", AGENT_ID, *capture_args, cwd=project) assert code == 0 and full["should_run"], full context = full["interaction_contract"]["agent_channel"]["work_context"] assert context["complete"] and context["selected_todo_text_verified"] @@ -115,6 +134,27 @@ def test_real_guard_keeps_multiline_task_once_without_differential_opt_in(tmp_pa "--todo-id", TODO_ID, cwd=project) assert code == 0 and detail["todo"]["text"] == body and detail["todo"]["title"] == body + if captured: + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + from test_complete_turn_decision import _canonical_digest, _resign_contract + from test_quota_settlement_cli import _spend_run_count + saved = json.loads((capture / "decision.json").read_text()) + assert saved == full + historical = build_turn_envelope(saved) + restored_task = next(source["content"]["todo"] for source in historical["work_context"]["sources"] + if source.get("source") == "selected_todo") + assert restored_task["text"] == body + invalid = deepcopy(saved) + invalid["agent_lane_next_action"]["title_ref"] = lane["title_ref"].replace(f"[{index}]", "[999]") + invalid, _ = _resign_contract(invalid) + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text(json.dumps(invalid)) + rc, refused = _run_cli(registry, runtime, "quota", "replan-view", + "--decision-file", str(invalid_path), "--source-hash", _canonical_digest(invalid), + "--replan-stage", "review", cwd=project) + assert rc != 0 and "Current Task title reference" in json.dumps(refused) + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + @pytest.mark.parametrize("mirror", ["shared", "identical", "different", "boolean-number", "missing-root"]) def test_fallback_reference_preserves_complete_same_packet_facts(mirror: str) -> None: diff --git a/tests/control_plane/test_quota_task_title_references.py b/tests/control_plane/test_quota_task_title_references.py new file mode 100644 index 0000000000..f72191880f --- /dev/null +++ b/tests/control_plane/test_quota_task_title_references.py @@ -0,0 +1,122 @@ +"""Full current-Task requirements survive composition, reference validation and recovery.""" +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import _deduplicate_agent_lane_next_action +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, build_turn_envelope, compose_turn_decision, + prepare_context_delivery, restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + body = "Keep the original acceptance, evidence and authorization. 完整要求🙂\n" * 100 + "Stop before publication." + source["selected_todo"] = {"todo_id": "current-task"} + source["agent_lane_next_action"] = {"todo_id": "current-task", "text": "Bounded label…", "title": body} + source["interaction_contract"]["agent_channel"]["work_context"] = { + "complete": True, "selected_todo_text_verified": True, "sources": [ + {"source": "selected_todo", "content": {"ok": True, "matched": True, + "read_complete": True, "todo": {"todo_id": "current-task", "status": "open", "text": body}}}]} + return source + + +def context(source): + return source["interaction_contract"]["agent_channel"]["work_context"] + + +@pytest.mark.parametrize("field", ["text", "title"]) +def test_exact_title_sharing_preserves_entire_task_in_complete_and_historical_readers(field): + source = packet() + body = source["agent_lane_next_action"]["title"] + if field == "title": + context(source)["sources"][0]["content"]["todo"].update(text="[P0] " + body, title=body) + original = deepcopy(source) + contract = compose_turn_decision(source) + expected = {"todo_id": "current-task", "text": "Bounded label…", "title_ref": + f"$.interaction_contract.agent_channel.work_context.sources[0].content.todo.{field}"} + assert contract["agent_lane_next_action"] == expected + assert _deduplicate_agent_lane_next_action(source["agent_lane_next_action"], payload=source) == expected + assert compose_turn_decision(contract) == contract and source == original + assert contract["interaction_contract"] == source["interaction_contract"] + assert contract["selected_todo"] == source["selected_todo"] + envelope = build_turn_envelope(contract) + assert envelope["work_context"]["sources"] == context(source)["sources"] + assert envelope["action_signature"]["matches"] is True + assert "agent_lane_next_action" not in envelope # Historical view retains the full declaration in work context. + + +@pytest.mark.parametrize("mutation", ["dangling", "malformed", "both", "chain", "wrong-source", "duplicate", + "wrong-id", "wrong-selection", "boolean-id", "incomplete", "unverified", "unmatched", "ambiguous", + "partial", "failed", "archived", "done", "nonstring", "empty"]) +def test_invalid_task_reference_is_refused_before_consumption_even_with_valid_integrity(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + contract = compose_turn_decision(packet()) + envelope = build_turn_envelope(contract) + lane = contract["agent_lane_next_action"] + ctx = context(contract) + src = ctx["sources"][0] + content = src["content"] + todo = content["todo"] + if mutation == "dangling": + lane["title_ref"] = lane["title_ref"].replace("[0]", "[99]") + elif mutation == "malformed": + lane["title_ref"] = {"path": lane["title_ref"]} + elif mutation == "both": + lane["title"] = todo["text"] + elif mutation == "chain": + todo["text_ref"] = "$.another" + elif mutation == "wrong-source": + src["source"] = "historical_todo" + elif mutation == "duplicate": + ctx["sources"].append(deepcopy(src)) + elif mutation == "wrong-id": + todo["todo_id"] = "other-task" + elif mutation == "wrong-selection": + contract["selected_todo"]["todo_id"] = "other-task" + elif mutation == "boolean-id": + lane["todo_id"] = todo["todo_id"] = contract["selected_todo"]["todo_id"] = True + elif mutation in {"incomplete", "unverified"}: + ctx["complete" if mutation == "incomplete" else "selected_todo_text_verified"] = False + elif mutation in {"unmatched", "partial", "failed"}: + content[{"unmatched": "matched", "partial": "read_complete", "failed": "ok"}[mutation]] = False + elif mutation == "ambiguous": + content["ambiguous"] = True + elif mutation == "archived": + todo["archive_state"] = "archive" + elif mutation == "done": + todo["status"] = "done" + else: + todo["text"] = "" if mutation == "empty" else True + contract, _ = _resign_contract(contract) + for read in (lambda: compose_turn_decision(contract), lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result("replan_view", payload=contract, + source_hash=_canonical_digest(contract), stage="review")): + with pytest.raises(RuntimeError, match="Current Task title reference"): + read() + + +@pytest.mark.parametrize("complete", [False, True]) +def test_new_requirements_survive_delta_and_lost_baseline_recovery(complete): + source = packet() + before = compose_turn_decision(source) + body = source["agent_lane_next_action"]["title"] + " New requirement: preserve failed evidence." + source["agent_lane_next_action"]["title"] = body + context(source)["sources"][0]["content"]["todo"]["text"] = body + context(source)["complete"] = complete + after = compose_turn_decision(source) + scope = dict(goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one") + baseline = prepare_context_delivery(before, scope=scope, capture_id="one", surface="complete_turn") + full = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn") + delta = prepare_context_delivery(after, scope=scope, capture_id="two", surface="complete_turn", + baseline=baseline, retained={"complete": True, "target": baseline["target"]}) + assert delta["mode"] == ("delta" if complete else "full") + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)["context"] + assert restored == after + assert context(restored)["sources"][0]["content"]["todo"]["text"] == body + assert build_turn_envelope(restored)["work_context"]["sources"] == context(source)["sources"] From 1ae001e492bd1a85eb4853ec500a42e7893c484e Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:06:09 +0800 Subject: [PATCH 24/33] docs(rfc): record task sharing and original quota cost limits Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 34 +++++++++++++++++-- ...ller-executor-unified-contract-v0.zh-CN.md | 24 +++++++++++-- .../quota-cli-hot-path-compaction-v0.md | 18 ++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index eecb5f77f2..1de1f35133 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -709,7 +709,7 @@ reads. The original display-sharing helper is frozen at `895f38fc5` for this comparison. The acceptance-free workload above remains unchanged. This absorbs an existing quota efficiency into the common path; it does not establish an advantage over quota or full-cost benefit. The subsequent guidance increment below narrows that gap; next qualify the remaining -vision/title sharing in their existing owners before deferred-detail choices. Keep cold and staged totals separate and retain negative results; do +vision sharing in its existing owner after the title increment below, before deferred-detail choices. Keep cold and staged totals separate and retain negative results; do not force a byte target by dropping unique instructions. The next #7430 increment shares replan vision-authoring instructions through @@ -731,7 +731,7 @@ remains inline; sharing never invents that owner or transfers settlement authori At the authoring checkpoint, four legacy projection tests expected multiline-title or Explore-guidance sharing absent from the complete candidate and failed at both the preceding and authoring heads. The guidance increment below resolves two; -the two multiline-title cases remain visible C1 migration gaps, not waived gates. +the title increment below resolves the other two. Historical failures remain recorded, not waived or relabeled. These slices do not qualify installed App adoption. For the fixed synthetic authoring fixture, the original non-staged CLI is 22,062 @@ -782,6 +782,36 @@ non-staged. Required extra reads, framing, tokens/cache, latency and accepted mo outcomes remain unmeasured. Shared code and restored quota information efficiency do not establish a positive complete-cost diff; C1 remains open. +The current-Task title increment removes the Python exact-source matching algorithm. +Complete composition and the legacy display delegate now share its TypeScript +owner. Only an exact duplicate of `text` or `title` in the unique, verified, +complete current selected Task can become a same-capture `title_ref`; the full +original declaration and its provenance stay inline. Missing, differing, +incomplete, ambiguous, failed, done or archived sources do not qualify. The +historical character threshold, including Unicode code points, stays unchanged. +The old display-only label-prefix omission remains local and is not applied to +the complete contract. This delegate retires with the legacy display callers. + +Complete readers validate identity and reference ownership again, rejecting +conflicting inline values, chains, malformed paths and unavailable targets even +with valid hashes. Historical envelopes retain the full Task source, without a +relocated dangling lane reference. Real File/SQLite CLI with differential delivery +off now passes the two previous multiline-title cases; captured variants also +verify exact readback and refusal before state mutation or spend. Delta and +lost-baseline/full recovery preserve changed requirements. Existing independent +Todo detail still returns the full declaration. This qualifies local transport +semantics, not installed App adoption or model behavior. + +For a fixed synthetic current-Task fixture, the original non-staged CLI frozen +at `895f38fc5` is 28,585 compact UTF-8 JSON bytes and the fused contract is 29,780 +(+4.18%). Captured original entry/review/writeback total 29,906 versus 29,863 fused +bytes. The no-current-Task control remains 20,543 versus 21,738 non-staged bytes. +These totals exclude request framing, extra material reads, tokens/cache, latency +and accepted outcomes. They restore quota's existing sharing in the common path; +they do not establish positive full-cost benefit. Vision audit sharing, rejected +admissions, remaining consumer retirement and packaged/outcome qualification +remain C1 work. + Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, latency, failures/retries, verification and human intervention. A smaller first diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 2d7d3a6054..0bf3bb7f2f 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -534,7 +534,7 @@ composition 调用内部完成共享。未声称延迟改善。 原 entry/review/writeback 合计为 24,509,未计追加材料读取。对照冻结 `895f38fc5` 的原展示共享 helper;上述不含验收副本的 workload 数字不变。这把 quota 已有的效率 吸收到共同路径,不能证明优于 quota 或完整成本获益。下述 guidance 增量进一步缩小缺口;下一步先在既有 owner 验证剩余 -vision/title 共享,再评估延后详情。冷启动与分阶段总量分别保留, +vision 共享;标题由下述增量处理,然后再评估延后详情。冷启动与分阶段总量分别保留, 保留负结果,不能靠删除独有指令达到字节目标。 #7430 后续增量通过同一个 TypeScript composition 边界共享 replan 的 vision-authoring @@ -548,7 +548,7 @@ vision/title 共享,再评估延后详情。冷启动与分阶段总量分 输入分别验证。已验证的 Native 外层 controller 输入没有内联结算 authoring owner,authoring 仍保留原文; 共享不会虚构该 owner 或转移结算权。authoring 检查点的四项旧 projection 测试期待 完整候选尚未采用的多行标题或 Explore 指导共享,在此前及 authoring head 都失败。 -下述 guidance 增量解决其中两项;两项多行标题用例仍是 C1 迁移缺口,不豁免门禁。 +下述 guidance 增量解决其中两项,标题增量解决另外两项。历史失败继续保留,不豁免或改写。 这些切片不证明已安装 App 的采用。 固定的合成 authoring fixture 中,原未分阶段 CLI 为 22,062 个紧凑 UTF-8 JSON 字节, @@ -586,6 +586,26 @@ entry/review/writeback 合计 26,081,融合为 24,403。不含 guidance 的 fi 模型验收成果尚未测量。代码共享及恢复 quota 已有的信息效率不能证明完整成本 diff 正向; C1 继续保持未完成。 +当前 Task 标题增量删除 Python 的精确来源匹配算法,完整 composition 和旧展示委托 +共用 TypeScript owner。只有与唯一、已核验、完整的当前选中 Task 的 `text` 或 `title` +精确重复时,才转换为同一 capture 的 `title_ref`;完整原始声明及来源继续内联。 +来源缺失、不同、不完整、歧义、失败、已完成或已归档均不符合共享条件。历史字符阈值 +(包括 Unicode 码点计数)保持不变。旧展示按 label 前缀省略标题的逻辑留在展示层, +不应用于完整契约;委托 helper 随旧展示调用者退役。 + +完整 reader 再次验证身份及引用归属,即使 hash 有效,也拒绝冲突原文、引用链、 +非法路径及不可用目标。历史 envelope 保留完整 Task 来源,不携带移动后悬空的 lane +引用。真实 File/SQLite CLI 在差分关闭时已通过此前两项多行标题用例;capture 变体 +还验证精确回读及状态/额度变化前拒绝。差分及 baseline 丢失/全量恢复保留变化后的要求; +独立 Todo detail 仍返回完整声明。这证明本地传输语义,不证明已安装 App 或模型行为。 + +固定合成当前 Task fixture 中,冻结 `895f38fc5` 的原未分阶段 CLI 为 28,585 个紧凑 +UTF-8 JSON 字节,融合契约为 29,780(+4.18%)。启用 capture 后,原 +entry/review/writeback 合计 29,906,融合为 29,863。不含当前 Task 的控制样例 +未分阶段仍为 20,543 对 21,738 字节。这些总量未含请求包装、追加材料读取、tokens/cache、 +延迟及验收成果;它们将 quota 已有共享恢复到共同路径,不能证明完整成本收益正向。 +vision audit 共享、拒绝准入、剩余 consumer 退役及打包/成果资格仍是 C1 工作。 + 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 只有必要语义保留且同等验收成果的完整成本降低,或可比预算下验收成果改善,才判正向。 diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index daf54fa304..badfa7ca47 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -85,6 +85,24 @@ neither enables native tracing nor changes the default-off transport switch. 控制字段和原始捕获均保留。`--include-detail agent-todos` 保留内联标题。 这是普通 CLI 序列化去重,差分开关关闭和开启时均适用,不会开启原生追踪或改变默认关闭。 +During the C1 migration, exact current-Task title sharing is owned by the +TypeScript decision composer in `turn_envelope.ts`. The legacy Python display +helper delegates that algorithm; its label-prefix omission stays display-only. +Complete `loopx_turn_envelope_v1` decisions share before hashing/capture and +validate the same-capture target on recovery. Conflicting inline/reference values, +chains, malformed paths or a no-longer-verified selected Task reject even with +valid integrity hashes. This source sharing does not require differential delivery. +The `--include-detail` behavior above describes the historical display projection; +it does not select an alternative complete contract. Retire its delegate with the +legacy display callers while retaining supported historical capture recovery. + +C1 迁移中,当前 Task 标题的精确共享归 `turn_envelope.ts` 的 TypeScript decision +composer 所有;旧 Python 展示 helper 委托该算法,按 label 前缀省略仍仅属于展示。 +完整 `loopx_turn_envelope_v1` 在 hash/capture 前共享,恢复时验证同一 capture 的目标。 +原文/引用冲突、引用链、非法路径或当前选中 Task 无法继续核验时,即使完整性 hash +有效也拒绝。该来源共享不要求开启差分。上文 `--include-detail` 描述历史展示投影, +不会选择另一套完整契约;委托随旧展示调用者退役,支持的历史 capture 恢复继续保留。 + When a replan action carries a complete `vision_authoring` schema, the default `quota should-run` packet now retains it, alongside its executable writeback summary (`required_fields`, accepted path outcomes, and rule). Only when the From faf3a95d0da56578c53f800e41b3eaf0484fbcbc Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:28:15 +0800 Subject: [PATCH 25/33] refactor(quota): share complete vision audits through one owner Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/cli_projection.py | 61 +-- loopx/control_plane/quota/turn_envelope.ts | 65 +++ .../test_quota_vision_audit_references.py | 370 ++++++++++++++++++ 3 files changed, 441 insertions(+), 55 deletions(-) create mode 100644 tests/control_plane/test_quota_vision_audit_references.py diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 4de759a0c5..cd3f717e7d 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -459,54 +459,11 @@ def _reference_inline_replan_authoring( }} -def _vision_continuation_ref(audit: dict[str, Any]) -> dict[str, Any]: - compact = { - key: audit[key] - for key in ("schema_version", "required", "decision") - if key in audit - } - judge = audit.get("vision_gap_judge") - if isinstance(judge, dict): - compact["vision_gap_judge"] = { - key: judge[key] for key in ("done", "decision") if key in judge - } - compact["vision_gap_judge"]["projection_ref"] = ( - "$.vision_continuation_audit.vision_gap_judge" - ) - compact["projection_ref"] = "$.vision_continuation_audit" - return compact - - -def _replace_nested_vision_continuation_audits( - payload: dict[str, Any], - *, - source_audit: dict[str, Any], - audit_ref: dict[str, Any], -) -> None: - frontier = payload.get("goal_frontier_projection") - if ( - isinstance(frontier, dict) - and frontier.get("vision_continuation_audit") == source_audit - ): - compact_frontier = dict(frontier) - compact_frontier["vision_continuation_audit"] = dict(audit_ref) - payload["goal_frontier_projection"] = compact_frontier +def _reference_vision_continuation(payload: dict[str, Any]) -> dict[str, Any]: + """Legacy display delegates exact audit sharing to the complete owner.""" + from .turn_envelope import _turn_envelope_result - interaction = payload.get("interaction_contract") - if not isinstance(interaction, dict): - return - compact_interaction = dict(interaction) - for channel_name in ("agent_channel", "cli_channel"): - channel = compact_interaction.get(channel_name) - if ( - not isinstance(channel, dict) - or channel.get("vision_continuation_audit") != source_audit - ): - continue - compact_channel = dict(channel) - compact_channel["vision_continuation_audit"] = dict(audit_ref) - compact_interaction[channel_name] = compact_channel - payload["interaction_contract"] = compact_interaction + return _turn_envelope_result("reference_vision_continuation", payload=payload) def _compact_capability_gate(gate: dict[str, Any]) -> dict[str, Any]: @@ -745,14 +702,8 @@ def compact_quota_should_run_cli_payload( compact["goal_boundary"] = compact_goal_boundary vision_audit = payload.get("vision_continuation_audit") if not include_vision_detail and isinstance(vision_audit, dict): - compact = dict(compact) - compact_vision_audit = _compact_vision_continuation_audit(vision_audit) - compact["vision_continuation_audit"] = compact_vision_audit - _replace_nested_vision_continuation_audits( - compact, - source_audit=vision_audit, - audit_ref=_vision_continuation_ref(compact_vision_audit), - ) + compact = _reference_vision_continuation(compact) + compact["vision_continuation_audit"] = _compact_vision_continuation_audit(vision_audit) replan_action = payload.get("replan_action_packet") if not include_vision_detail and isinstance(replan_action, dict): compact_replan_action = _reference_inline_replan_authoring( diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index b7854c721e..a776c9bfc4 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -403,6 +403,66 @@ function referenceExploreGuidance(payload: JsonObject): JsonObject { work_context: {...context, sources}}}}; } +/** Keep the complete vision audit once. These existing projection references + * share identical facts only; a channel-specific obligation is never a summary + * of the root. The old display adapter delegates here before adding metadata. */ +function visionAuditReference(audit: JsonObject): JsonObject { + const reference: JsonObject = {projection_ref: "$.vision_continuation_audit"}; + for (const key of ["schema_version", "required", "decision"]) { + if (Object.hasOwn(audit, key)) reference[key] = audit[key]; + } + if (typeof audit.vision_gap_judge === "object" && audit.vision_gap_judge !== null && + !Array.isArray(audit.vision_gap_judge)) { + const judge = object(audit.vision_gap_judge); + reference.vision_gap_judge = {projection_ref: "$.vision_continuation_audit.vision_gap_judge"}; + for (const key of ["done", "decision"]) { + if (Object.hasOwn(judge, key)) (reference.vision_gap_judge as JsonObject)[key] = judge[key]; + } + } + return reference; +} + +function mapNestedVisionAudits(payload: JsonObject, visit: (audit: JsonObject) => JsonObject): JsonObject { + const map = (value: unknown) => { + const parent = object(value), audit = parent.vision_continuation_audit; + if (typeof audit !== "object" || audit === null || Array.isArray(audit)) return value; + return {...parent, vision_continuation_audit: visit(audit as JsonObject)}; + }; + const rawInteraction = payload.interaction_contract, interaction = object(rawInteraction); + const hasInteraction = typeof rawInteraction === "object" && rawInteraction !== null && !Array.isArray(rawInteraction); + return {...payload, + ...(Object.hasOwn(payload, "goal_frontier_projection") ? {goal_frontier_projection: map(payload.goal_frontier_projection)} : {}), + ...(hasInteraction ? {interaction_contract: {...interaction, + ...Object.fromEntries(["agent_channel", "cli_channel"].filter(key => Object.hasOwn(interaction, key)) + .map(key => [key, map(interaction[key])]))}} : {})}; +} + +function requireVisionAuditReferences(payload: JsonObject): void { + const owner = object(payload.vision_continuation_audit); + const chained = Object.hasOwn(owner, "projection_ref") || Object.hasOwn(object(owner.vision_gap_judge), "projection_ref"); + if (chained) throw new EffectRuntimeRequestError("Vision audit reference requires a complete inline root owner"); + mapNestedVisionAudits(payload, audit => { + if (Object.hasOwn(audit, "projection_ref") || Object.hasOwn(object(audit.vision_gap_judge), "projection_ref")) { + if (!Object.keys(owner).length || + JSON.stringify(canonicalValue(audit)) !== JSON.stringify(canonicalValue(visionAuditReference(owner)))) { + throw new EffectRuntimeRequestError("Vision audit reference must match its complete inline root owner without conflicting facts"); + } + } + return audit; + }); +} + +function referenceVisionContinuation(payload: JsonObject): JsonObject { + requireVisionAuditReferences(payload); + const owner = object(payload.vision_continuation_audit); + if (!Object.keys(owner).length) return payload; + const encoded = JSON.stringify(canonicalValue(owner)), reference = visionAuditReference(owner); + // A short audit is cheaper inline. Do not create a reference merely to count sharing. + if (Buffer.byteLength(encoded, "utf8") <= Buffer.byteLength(JSON.stringify(reference), "utf8")) return payload; + return mapNestedVisionAudits(payload, audit => + JSON.stringify(canonicalValue(audit)) === encoded ? structuredClone(reference) : audit); +} + /** The verified current Task is the only owner of a shared lane title. Keep * the original declaration inline and require exact identity, never a label or * prefix comparison. The legacy display and complete writer use this owner. */ @@ -1405,6 +1465,7 @@ function requireCompleteDecisionSource(source: JsonObject): void { // object (including optional facts) rather than its normalized decode. decodeInteractionContract(source.interaction_contract); replanAuthoringContract(source); + requireVisionAuditReferences(source); requireCurrentTaskTitleReference(source); resolveExploreGuidance(source, object(object(object(source.interaction_contract).agent_channel).work_context)); // This root field selects execution, unlike optional evidence carried intact @@ -1503,6 +1564,7 @@ export function composeCompleteTurnDecision(value: unknown): JsonObject { source = referenceReplanAuthoring(source); source = referenceExploreGuidance(source); source = referenceCurrentTaskTitle(source); + source = referenceVisionContinuation(source); requireCompleteDecisionSource(source); const result: JsonObject = {...source, schema_version: COMPLETE_TURN_DECISION_VERSION, contract_binding: {scheduler_execution_args: request.scheduler_execution_args, @@ -1612,6 +1674,9 @@ function bindTurnDecisionCapture(request: JsonObject): JsonObject { export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; + if (operation === "reference_vision_continuation") { + return referenceVisionContinuation(requireJsonObject(request.payload, "vision audit sharing source")); + } if (operation === "reference_current_task_title") { return referenceCurrentTaskTitle(requireJsonObject(request.payload, "current Task title sharing source")); } diff --git a/tests/control_plane/test_quota_vision_audit_references.py b/tests/control_plane/test_quota_vision_audit_references.py new file mode 100644 index 0000000000..d68e6b7ada --- /dev/null +++ b/tests/control_plane/test_quota_vision_audit_references.py @@ -0,0 +1,370 @@ +"""Whole audit sharing preserves evidence, decisions and recovery obligations.""" + +from copy import deepcopy +import json + +import pytest + +from loopx.control_plane.quota.cli_projection import ( + compact_quota_should_run_cli_payload, +) +from loopx.control_plane.quota.turn_envelope import ( + _turn_envelope_result, + build_turn_envelope, + compose_turn_decision, + prepare_context_delivery, + restore_context_delivery, +) +from test_complete_turn_decision import _canonical_digest, _resign_contract +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + audit = { + "schema_version": "vision_continuation_audit_v0", + "required": True, + "decision": "continue", + "agent_id": "worker", + "trigger_count": 1, + "required_before_closeout": [ + "inspect_original_evidence", + "preserve_unmet_requirements", + ], + "vision_gap_judge": { + "done": False, + "decision": "continue", + "agent_judge_instruction": "Keep original requirements, negative evidence and stop conditions. " + * 20, + "done_only_when": ["authoritative_evidence_satisfies_acceptance"], + "continue_when": ["evidence_is_missing_weak_or_stale"], + }, + "acceptance_gaps": [ + {"requirement": "Original acceptance", "evidence": "fixture:negative"} + ], + "optional_future_fact": {"revision": 2, "negative": True}, + } + source["interaction_contract"]["agent_channel"]["work_context"] = { + "complete": True, + "sources": [], + } + source["vision_continuation_audit"] = audit + source["goal_frontier_projection"] = {"vision_continuation_audit": deepcopy(audit)} + for channel in ("agent_channel", "cli_channel"): + source["interaction_contract"][channel]["vision_continuation_audit"] = deepcopy( + audit + ) + return source + + +def nested(source): + return [ + source["goal_frontier_projection"], + source["interaction_contract"]["agent_channel"], + source["interaction_contract"]["cli_channel"], + ] + + +@pytest.fixture( + params=[compact_quota_should_run_cli_payload, compose_turn_decision], + ids=["legacy", "complete"], +) +def share(request): + return request.param + + +def test_exact_audit_is_retained_once_with_complete_judge_and_history(share): + source = packet() + original = deepcopy(source) + shared = share(source) + assert source == original + assert share(shared) == shared + audit = shared["vision_continuation_audit"] + assert {k: v for k, v in audit.items() if k != "payload_compaction"} == source[ + "vision_continuation_audit" + ] + for carrier in nested(shared): + assert carrier["vision_continuation_audit"] == { + "schema_version": "vision_continuation_audit_v0", + "required": True, + "decision": "continue", + "vision_gap_judge": { + "done": False, + "decision": "continue", + "projection_ref": "$.vision_continuation_audit.vision_gap_judge", + }, + "projection_ref": "$.vision_continuation_audit", + } + assert ( + json.dumps(shared).count(audit["vision_gap_judge"]["agent_judge_instruction"]) + == 1 + ) + historical = build_turn_envelope(shared) + assert ( + historical["contract_capsule"]["vision_continuation_audit"]["vision_gap_judge"] + == audit["vision_gap_judge"] + ) + assert historical["action_signature"]["matches"] is True + assert "projection_ref" not in json.dumps( + historical["contract_capsule"]["vision_continuation_audit"] + ) + + +@pytest.mark.parametrize( + "difference", + ["requirement", "type", "version", "negative", "unknown", "missing-owner"], +) +def test_different_audit_is_never_replaced_by_root_summary(share, difference): + source = packet() + for carrier in nested(source): + audit = carrier["vision_continuation_audit"] + if difference == "requirement": + audit["required_before_closeout"].append("wait_for_review") + elif difference == "type": + audit["required"] = 1 + elif difference == "version": + audit["schema_version"] = "future_audit_v1" + elif difference == "negative": + audit["acceptance_gaps"][0]["evidence"] = "fixture:other" + elif difference == "unknown": + audit["optional_future_fact"]["revision"] = 3 + if difference == "missing-owner": + source.pop("vision_continuation_audit") + before = [deepcopy(c["vision_continuation_audit"]) for c in nested(source)] + assert [c["vision_continuation_audit"] for c in nested(share(source))] == before + + +@pytest.mark.parametrize( + "mutation", + [ + "missing", + "empty", + "malformed", + "wrong-owner", + "chain", + "judge-chain", + "conflict", + "type-conflict", + "extra-clause", + "judge-only", + ], +) +def test_dangling_or_conflicting_audit_reference_is_rejected_with_valid_hash(mutation): + from loopx.control_plane.turn_driver.codex_cli import _prompt + + contract = compose_turn_decision(packet()) + envelope = build_turn_envelope(contract) + audit = nested(contract)[1]["vision_continuation_audit"] + if mutation == "missing": + contract.pop("vision_continuation_audit") + elif mutation == "empty": + contract["vision_continuation_audit"] = {} + elif mutation == "malformed": + audit["projection_ref"] = {"path": "$.vision_continuation_audit"} + elif mutation == "wrong-owner": + audit["projection_ref"] = "$.another_audit" + elif mutation == "chain": + contract["vision_continuation_audit"]["projection_ref"] = "$.another_audit" + elif mutation == "judge-chain": + contract["vision_continuation_audit"]["vision_gap_judge"]["projection_ref"] = ( + "$.another_judge" + ) + elif mutation == "conflict": + audit["vision_gap_judge"]["done"] = True + elif mutation == "type-conflict": + audit["required"] = 1 + elif mutation == "extra-clause": + audit["required_before_closeout"] = ["skip_review"] + else: + audit.pop("projection_ref") + contract, _ = _resign_contract(contract) + for read in ( + lambda: compose_turn_decision(contract), + lambda: build_turn_envelope(contract), + lambda: _prompt({"work_contract": contract, "turn_envelope": envelope}), + lambda: _turn_envelope_result( + "replan_view", + payload=contract, + source_hash=_canonical_digest(contract), + stage="review", + ), + ): + with pytest.raises(RuntimeError, match="Vision audit reference"): + read() + + +@pytest.mark.parametrize( + "changed, complete", [(False, True), (True, True), (True, False)] +) +def test_audit_survives_delta_full_and_lost_baseline_recovery(changed, complete): + source = packet() + source["interaction_contract"]["agent_channel"]["work_context"]["complete"] = ( + complete + ) + before = compose_turn_decision(source) + if changed: + source["vision_continuation_audit"]["vision_gap_judge"][ + "agent_judge_instruction" + ] += " Wait for new evidence." + for carrier in nested(source): + carrier["vision_continuation_audit"] = deepcopy( + source["vision_continuation_audit"] + ) + else: + source["reason"] = "A new observation" + after = compose_turn_decision(source) + scope = dict( + goal_id="fixture", agent_id="worker", session_id="session", context_epoch="one" + ) + baseline = prepare_context_delivery( + before, scope=scope, capture_id="one", surface="complete_turn" + ) + full = prepare_context_delivery( + after, scope=scope, capture_id="two", surface="complete_turn" + ) + delta = prepare_context_delivery( + after, + scope=scope, + capture_id="two", + surface="complete_turn", + baseline=baseline, + retained={"complete": True, "target": baseline["target"]}, + ) + assert delta["mode"] == ("delta" if complete else "full") + for retained in (baseline, None): + restored = restore_context_delivery(delta, baseline=retained, current=full)[ + "context" + ] + assert restored == after + assert ( + restored["vision_continuation_audit"] == source["vision_continuation_audit"] + ) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_real_guard_capture_and_recovery_preserve_vision_before_effects( + tmp_path, monkeypatch, provider +): + from canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, + read_fixture_todos, + ) + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from test_quota_settlement_cli import ( + AGENT_ID, + GOAL_ID, + _run_cli, + _spend_run_count, + _write_fixture, + _configure_autonomous_replan_fixture, + ) + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + _configure_autonomous_replan_fixture(project, runtime, registry) + runs = runtime / "goals" / GOAL_ID / "runs" / "index.jsonl" + prior = { + "generated_at": "2026-01-01T00:03:00+00:00", + "goal_id": GOAL_ID, + "agent_id": AGENT_ID, + "classification": "prior_progress", + "agent_vision": { + "agent_id": AGENT_ID, + "state": "vision_active", + "vision_patch": { + "vision_summary": "Verify the original outcome.", + "acceptance_summary": "Keep evidence for all original requirements.", + }, + "path_delta": {"outcome": "continue", "evidence_refs": ["evidence:prior"]}, + }, + } + runs.write_text(runs.read_text() + json.dumps(prior) + "\n") + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=read_fixture_todos(state), handoff_mode="soft_claim" + ), + state_path=state, + provider=provider, + ) + original_state = state.read_bytes() + capture = tmp_path / "captured" + code, full = _run_cli( + registry, + runtime, + "quota", + "should-run", + "--goal-id", + GOAL_ID, + "--agent-id", + AGENT_ID, + "--codex-app", + "--turn-instance-id", + "vision-read", + "--decision-output-dir", + str(capture), + cwd=project, + ) + assert code == 0 and full["should_run"], full + assert "context_delivery" not in full + saved = json.loads((capture / "decision.json").read_text()) + assert saved == full + audit = full["vision_continuation_audit"] + assert "projection_ref" not in audit + assert ( + full["goal_frontier_projection"]["vision_continuation_audit"]["projection_ref"] + == "$.vision_continuation_audit" + ) + assert ( + build_turn_envelope(saved)["contract_capsule"]["vision_continuation_audit"][ + "vision_gap_judge" + ] + == audit["vision_gap_judge"] + ) + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + invalid = deepcopy(saved) + invalid["goal_frontier_projection"]["vision_continuation_audit"][ + "required" + ] = not audit["required"] + invalid, _ = _resign_contract(invalid) + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text(json.dumps(invalid)) + rc, refused = _run_cli( + registry, + runtime, + "quota", + "replan-view", + "--decision-file", + str(invalid_path), + "--source-hash", + _canonical_digest(invalid), + "--replan-stage", + "review", + cwd=project, + ) + assert rc != 0 and "Vision audit reference" in json.dumps(refused) + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + + +def test_short_audit_remains_inline_without_reference_overhead(share): + source = packet() + source["vision_continuation_audit"] = {"required": False} + for carrier in nested(source): + carrier["vision_continuation_audit"] = {"required": False} + shared = share(source) + assert [c["vision_continuation_audit"] for c in nested(shared)] == [ + {"required": False} + ] * 3 + + +@pytest.mark.parametrize("interaction", [None, [], "historical-unavailable"]) +def test_legacy_sharing_does_not_manufacture_absent_interaction(interaction): + source = packet() + source["interaction_contract"] = interaction + shared = compact_quota_should_run_cli_payload(source) + assert shared["interaction_contract"] == interaction From 0d4ce5c6490291d205874f7c2cd3177decc9646b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:28:15 +0800 Subject: [PATCH 26/33] docs(rfc): qualify vision audit sharing and cost limits Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 30 +++++++++++++++++-- ...ller-executor-unified-contract-v0.zh-CN.md | 19 +++++++++++- .../quota-cli-hot-path-compaction-v0.md | 14 +++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 1de1f35133..0937c33ed0 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -808,9 +808,33 @@ at `895f38fc5` is 28,585 compact UTF-8 JSON bytes and the fused contract is 29,7 bytes. The no-current-Task control remains 20,543 versus 21,738 non-staged bytes. These totals exclude request framing, extra material reads, tokens/cache, latency and accepted outcomes. They restore quota's existing sharing in the common path; -they do not establish positive full-cost benefit. Vision audit sharing, rejected -admissions, remaining consumer retirement and packaged/outcome qualification -remain C1 work. +they do not establish positive full-cost benefit. + +The vision-audit increment now moves the nested exact-match algorithm from Python +into that same TypeScript owner. Complete and historical display callers share +only an identical full audit, including unknown facts, negative evidence and +ordered obligations, when its reference is smaller. Different JSON types are not +equal: the old Python `True == 1` comparison could erase a channel difference; +that case now stays inline. Short audits also stay inline. The root retains the +complete audit and judge; legacy display metadata remains in its adapter. + +Complete recovery rejects missing, malformed, chained or conflicting audit/judge +references even with recomputed valid hashes. Existing historical envelopes still +obtain their judge from the inline root, so no moved reference dangles; their +existing projection limits are unchanged. Real isolated File/SQLite CLI captures +with an active prior vision verify the full judge, exact capture readback and +rejection before state mutation or spend, without differential delivery enabled. +Delta/full and lost-baseline recovery retain changed requirements. Fixtures with +no active vision correctly emit no audit and cannot qualify this path. + +With the original CLI projection frozen at `895f38fc5`, the fixed synthetic audit +fixture uses 22,337 non-staged compact UTF-8 JSON bytes versus 23,333 fused (+4.46%). +Captured original entry/review/writeback total 23,658 versus 23,416 fused bytes. +The no-audit control remains 20,543 versus 21,738 non-staged bytes. These are not +tokens/cache, latency, extra reads, framing or accepted outcomes. Restored sharing +and fewer rule owners do not prove positive full-cost benefit. The legacy audit +delegate retires with its display callers; rejected admission, remaining consumer +retirement and packaged/outcome qualification still keep C1 open. Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 0bf3bb7f2f..b8c322b953 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -604,7 +604,24 @@ UTF-8 JSON 字节,融合契约为 29,780(+4.18%)。启用 capture 后, entry/review/writeback 合计 29,906,融合为 29,863。不含当前 Task 的控制样例 未分阶段仍为 20,543 对 21,738 字节。这些总量未含请求包装、追加材料读取、tokens/cache、 延迟及验收成果;它们将 quota 已有共享恢复到共同路径,不能证明完整成本收益正向。 -vision audit 共享、拒绝准入、剩余 consumer 退役及打包/成果资格仍是 C1 工作。 + +vision audit 增量现已将 Python 嵌套精确匹配算法迁入同一 TypeScript owner。完整契约 +和历史展示只在引用更小时共享完全相同的 audit,包括未知事实、负面证据及有序义务。 +不同 JSON 类型不相等:旧 Python 的 `True == 1` 比较可能抹掉 channel 差异,现保留 +该差异原文;短 audit 也保持内联。根保留完整 audit 和 judge,旧展示 metadata 留在 adapter。 + +完整恢复即使面对重新计算的有效 hash,也拒绝缺失、非法、链式或冲突的 audit/judge +引用。历史 envelope 仍从内联根取得 judge,不产生移动后的悬空引用;其已有投影限制 +不变。真实隔离 File/SQLite CLI 在含活动历史 vision、差分关闭时,验证完整 judge、 +精确 capture 回读及状态/额度变化前拒绝。差分/全量及 baseline 丢失恢复保留变化的 +要求。没有活动 vision 的 fixture 正确地不产生 audit,不能用来验收这条路径。 + +冻结原 CLI projection 于 `895f38fc5`,固定合成 audit 样例的未分阶段紧凑 UTF-8 JSON +为原 22,337 字节对融合 23,333(+4.46%);capture 原 entry/review/writeback 合计 +23,658 对融合 23,416。不含 audit 的控制样例仍为 20,543 对 21,738。它们不计 +tokens/cache、延迟、追加读取、请求包装及验收成果。恢复共享、减少规则 owner 仍不 +证明完整成本正向。旧 audit 委托随展示调用者退役;拒绝准入、剩余 consumer 退役及 +打包/成果资格继续使 C1 保持开放。 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index badfa7ca47..77a8ad2397 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -103,6 +103,20 @@ composer 所有;旧 Python 展示 helper 委托该算法,按 label 前缀省 有效也拒绝。该来源共享不要求开启差分。上文 `--include-detail` 描述历史展示投影, 不会选择另一套完整契约;委托随旧展示调用者退役,支持的历史 capture 恢复继续保留。 +The same composer also owns exact nested vision-audit sharing. The root keeps the +full audit and judge; the existing `projection_ref` carries only matching audit +and judge decision fields. Type-sensitive full-JSON comparison preserves different +channel facts and short audits inline. Complete readers reject unavailable owners, +reference chains and conflicting copies before effects. Historical envelopes keep +the inline-root judge, and the old display adds only its existing metadata. This +delegate retires with the same caller migration; it is not a second rule owner. + +同一 composer 也拥有嵌套 vision audit 的精确共享。根保留完整 audit 和 judge;已有 +`projection_ref` 仅携带匹配的 audit/judge 判定字段。完整 JSON 的类型敏感比较使 +不同 channel 事实和短 audit 保持内联。完整 reader 在产生效果前拒绝不可用 owner、 +引用链及冲突副本。历史 envelope 保留内联根 judge,旧展示只添加既有 metadata; +委托随同一调用链迁移退役,不构成第二个规则 owner。 + When a replan action carries a complete `vision_authoring` schema, the default `quota should-run` packet now retains it, alongside its executable writeback summary (`required_fields`, accepted path outcomes, and rule). Only when the From ddce750de90f0d2356f35ae7537dd8ec8f2009e9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:56:21 +0800 Subject: [PATCH 27/33] fix(quota): compose declined decisions through the unified contract Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/quota.py | 20 +- .../test_quota_decision_capture.py | 34 ++- .../test_quota_identity_precondition.py | 13 + .../test_quota_rejected_decision_contract.py | 260 ++++++++++++++++++ .../test_turn_envelope_capability_facts.py | 11 +- tests/test_turn_envelope.py | 4 +- 6 files changed, 314 insertions(+), 28 deletions(-) create mode 100644 tests/control_plane/test_quota_rejected_decision_contract.py diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index 04c705fa15..edac3cfd2c 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -405,18 +405,20 @@ def _project_quota_cli_payload( *, captured_decision_path: str | None = None, ) -> dict[str, object]: - """Project already-decided facts; preserve typed failures on envelope rejection. + """Carry complete decisions and predecision diagnostics without a second view. - The envelope is an additive hot-path view over a decided payload. A typed - validation/failure payload has no interaction contract to project, so a - renderer rejection keeps the typed diagnostic itself (with the skip reason) - instead of masking it with a crash (issue #3687). + A collection/validation failure has no executable interaction to compose. + Preserve that original diagnostic and recovery instead of attempting the + historical envelope renderer or manufacturing a work contract. """ from ..control_plane.effect_program import reject_retired_quota_format reject_retired_quota_format(payload) - if args.quota_command == "should-run" and payload.get("schema_version") == "loopx_turn_envelope_v1": - return payload + if args.quota_command == "should-run": + if payload.get("schema_version") == "loopx_turn_envelope_v1" or ( + payload.get("ok") is False and not isinstance(payload.get("interaction_contract"), Mapping) + ): + return payload if not bool(getattr(args, "turn_envelope", False)): if args.quota_command in {"status", "plan"}: return compact_quota_plan_cli_payload(payload, detail_sections=detail_sections) @@ -494,7 +496,9 @@ def _emit_quota_result( payload, registry_path=registry_path, ) - if payload.get("ok") is True and args.quota_command == "should-run" and isinstance(payload.get("interaction_contract"), Mapping): + # A decided refusal has the same complete contract as an admission. `ok` + # still controls exit status and host handoff; composition grants no work. + if args.quota_command == "should-run" and isinstance(payload.get("interaction_contract"), Mapping): payload = compose_turn_decision(payload, scheduler_execution_context=context.scheduler_context if context is not None else None) try: capture_decision(capture_directory, payload) diff --git a/tests/control_plane/test_quota_decision_capture.py b/tests/control_plane/test_quota_decision_capture.py index 58dafe2c15..16f42942e7 100644 --- a/tests/control_plane/test_quota_decision_capture.py +++ b/tests/control_plane/test_quota_decision_capture.py @@ -103,8 +103,9 @@ def test_real_cli_fallback_reference_keeps_capture_and_turn_effects(tmp_path, mo fallback = full['blocked_priority_fallback'] assert full['heartbeat_recommendation']['blocked_priority_fallback'] == fallback assert ordinary['blocked_priority_fallback'] == fallback - assert ordinary['heartbeat_recommendation']['blocked_priority_fallback_ref'] == '$.blocked_priority_fallback' - assert 'blocked_priority_fallback' not in ordinary['heartbeat_recommendation'] + assert ordinary == full + assert ordinary['schema_version'] == 'loopx_turn_envelope_v1' + assert ordinary['heartbeat_recommendation']['blocked_priority_fallback'] == fallback assert ordinary['heartbeat_receipt'] == full['heartbeat_receipt'] assert ordinary['interaction_contract']['cli_channel']['settlement_plan'] == full[ 'interaction_contract']['cli_channel']['settlement_plan'] @@ -156,20 +157,23 @@ def test_real_cli_boundary_clauses_reach_shared_host_without_loss(tmp_path, monk full = json.loads(original_bytes) assert full['goal_boundary']['write_scope'] == scopes assert full['goal_boundary']['guards'] == guards + assert envelope == full + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(full) for field in ('write_scope', 'guards', 'requires_parent_approval'): assert envelope['boundary'][field] == full['goal_boundary'][field], field - authority = extract_turn_authority({'turn_envelope': envelope}) + authority = extract_turn_authority({'work_contract': full, 'turn_envelope': envelope}) assert authority['write_scope'] == scopes assert authority['boundary']['guards'] == guards assert authority['boundary']['requires_parent_approval'] == ['publish', 'production-action'] assert 'write_scope' not in authority['boundary'] # Existing aliases carry these facts once. prompt = render_prompt(authority) - transported, _ = json.JSONDecoder().raw_decode(prompt.split('Turn authority JSON:\n', 1)[1]) - assert transported == authority + transported, _ = json.JSONDecoder().raw_decode(prompt.split('Turn request:\n', 1)[1]) + assert transported == {'work_contract': full} altered = json.loads(json.dumps(envelope)) altered['boundary']['guards'][-1] = 'Continue without the final restriction.' with pytest.raises(ValueError, match='action signature'): - extract_turn_authority({'turn_envelope': altered}) + extract_turn_authority({'work_contract': full, 'turn_envelope': altered}) before = _heartbeat_receipt_count(runtime, TURN_ID) code, replay = _run_cli(registry, runtime, *args) assert code == 0, replay @@ -209,9 +213,14 @@ def test_real_guard_saved_before_envelope_and_read_without_reexecution(tmp_path, code, envelope = _run_cli(registry, runtime, *args, '--turn-envelope', '--decision-output-dir', str(capture)) assert code == 0, envelope - assert envelope['schema_version'] == 'loopx_turn_envelope_v0' + assert envelope['schema_version'] == 'loopx_turn_envelope_v1' saved = capture / 'decision.json' full = json.loads(saved.read_text()) + assert envelope == full + # Retain historical long-route capture recovery coverage explicitly; + # the current producer no longer selects this view with --turn-envelope. + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(full, captured_decision_path=str(saved)) app = envelope['scheduler']['app_automation'] assert 'ack_cli_args' not in app ref = app['ack_cli_args_detail_ref'] @@ -262,6 +271,8 @@ def test_real_guard_saved_before_envelope_and_read_without_reexecution(tmp_path, assert json.loads((second / 'decision.json').read_text())['heartbeat_receipt']['status'] == 'replayed' assert json.loads(saved.read_text()) == full + assert fresh == json.loads((second / 'decision.json').read_text()) + fresh = build_turn_envelope(fresh, captured_decision_path=str(second / 'decision.json')) assert shlex.split(fresh['detail_ref']['full_decision']) == ['cat', '--', str(second / 'decision.json')] # Missing old evidence fails locally; it never substitutes a new guard observation. saved.unlink() @@ -552,7 +563,8 @@ def typed_host(observed): 'execution_authority_granted': False, } assert code == 0 and selected['ok'] is True, selected - assert selected['schema_version'] == 'loopx_turn_envelope_v0' + assert selected['schema_version'] == 'loopx_turn_envelope_v1' + assert selected == observed['decision'] assert len(saved) == 1 and json.loads(saved[0].read_text()) == observed['decision'] @@ -595,9 +607,9 @@ def test_native_recovery_failure_keeps_original_receipt_and_capture( def refuse_recovery(payload, **kwargs): saved = kwargs['capture_directory'] / 'decision.json' attempts.append(saved.read_bytes()) - if turn_envelope: - assert payload['schema_version'] == 'loopx_turn_envelope_v0' - assert 'heartbeat_receipt' not in payload + assert payload['schema_version'] == 'loopx_turn_envelope_v1' + assert payload == json.loads(attempts[-1]) + assert payload['heartbeat_receipt']['turn_instance_id'] == TURN_ID raise QuotaDecisionTransportError( 'Current context recovery could not be published or safely invalidated. ' 'Read the saved original decision/receipt; do not rerun admission.' diff --git a/tests/control_plane/test_quota_identity_precondition.py b/tests/control_plane/test_quota_identity_precondition.py index 7ba657dfa8..02ed4c1217 100644 --- a/tests/control_plane/test_quota_identity_precondition.py +++ b/tests/control_plane/test_quota_identity_precondition.py @@ -37,9 +37,12 @@ def _write_state(project: Path) -> None: ) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("envelope_flag", [False, True]) def test_selected_registry_missing_roster_returns_typed_public_recovery( tmp_path: Path, capsys: pytest.CaptureFixture[str], + monkeypatch, provider: str, envelope_flag: bool, ) -> None: project = tmp_path / "project" global_project = tmp_path / "global-project" @@ -68,6 +71,13 @@ def test_selected_registry_missing_roster_returns_typed_public_recovery( registered_agents=[AGENT_ID], ) + from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_source_fixture, isolate_sqlite_runtime, + ) + isolate_sqlite_runtime(tmp_path, monkeypatch) + initialize_canonical_source_fixture(runtime_root, GOAL_ID, + state_path=project_state_path(project, GOAL_ID), provider=provider) + exit_code = main( [ "--format", @@ -78,6 +88,7 @@ def test_selected_registry_missing_roster_returns_typed_public_recovery( str(runtime_root), "quota", "should-run", + *(["--turn-envelope"] if envelope_flag else []), "--goal-id", GOAL_ID, "--agent-id", @@ -99,6 +110,8 @@ def test_selected_registry_missing_roster_returns_typed_public_recovery( assert "selected registry" in payload["recommended_action"] assert "--agent-id" in payload["recommended_action"] assert "verbose_debug" not in payload + assert "contract_binding" not in payload and "interaction_contract" not in payload + assert "turn_envelope_skipped" not in payload @pytest.mark.parametrize( diff --git a/tests/control_plane/test_quota_rejected_decision_contract.py b/tests/control_plane/test_quota_rejected_decision_contract.py new file mode 100644 index 0000000000..5007424db6 --- /dev/null +++ b/tests/control_plane/test_quota_rejected_decision_contract.py @@ -0,0 +1,260 @@ +"""Rejected decisions share the complete transport without acquiring authority.""" + +from argparse import Namespace +from copy import deepcopy +import json + +import pytest + +from loopx.cli_commands.quota import _project_quota_cli_payload +from loopx.control_plane.quota.turn_envelope import ( + build_turn_envelope, + compose_turn_decision, +) +from loopx.control_plane.turn_driver.driver import build_loopx_turn_plan +from canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, + read_fixture_todos, +) +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from test_quota_settlement_cli import ( + AGENT_ID, + GOAL_ID, + TODO_ID, + _run_cli, + _write_fixture, + _heartbeat_receipt_count, + _spend_run_count, + _configure_selectable_alternative, +) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("profile", ["codex_app_heartbeat", "generic_cli"]) +@pytest.mark.parametrize("rejection", ["missing", "capability", "reentry"]) +def test_rejected_cli_decision_is_complete_and_keeps_original_effects( + tmp_path, monkeypatch, provider, profile, rejection +): + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture( + tmp_path, required_capability="network" if rejection == "capability" else None + ) + if rejection == "reentry": + _configure_selectable_alternative(project) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority( + runtime, + GOAL_ID, + build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state) + ), + state_path=state, + provider=provider, + ) + before = state.read_bytes() + turn = "rejected-complete-contract" + args = [ + "quota", + "should-run", + "--goal-id", + GOAL_ID, + "--agent-id", + AGENT_ID, + "--runtime-profile", + profile, + "--turn-instance-id", + turn, + ] + original = None + if rejection == "reentry": + rc, original = _run_cli(registry, runtime, *args, cwd=project) + assert rc == 0, original + receipt_count = _heartbeat_receipt_count(runtime, turn) + assert receipt_count == (1 if original else 0) + for flag in ([], ["--turn-envelope"]): + capture = tmp_path / ("flag" if flag else "ordinary") + context = tmp_path / "context" + rc, denied = _run_cli( + registry, + runtime, + *args, + *flag, + "--todo-id", + TODO_ID if rejection == "capability" else "todo_unavailable", + "--decision-output-dir", + str(capture), + *( + ["--context-delivery", "on", "--codex-context-root", str(context)] + if profile == "codex_app_heartbeat" + else [] + ), + cwd=project, + ) + assert rc == 1 and denied["ok"] is False and denied["should_run"] is False, ( + denied + ) + assert denied["error_code"] == "quota_action_selection_rejected" + assert denied["schema_version"] == "loopx_turn_envelope_v1" + assert denied == json.loads((capture / "decision.json").read_text()) + assert "context_delivery" not in denied + assert ( + not context.exists() + ) # Refusal does not advance a host delivery baseline. + assert denied["interaction_contract"]["agent_channel"]["must_attempt"] is False + assert ( + denied["interaction_contract"]["agent_channel"]["delivery_allowed"] is False + ) + cli = denied["interaction_contract"]["cli_channel"] + assert ( + cli["spend_allowed_now"] is False and cli["spend_after_validation"] is False + ) + assert "settlement_plan" not in cli + assert "selected_todo" not in denied + assert ( + "selection_rejection" in denied + or "action_selection_qualification" in denied + ) + binding = denied["contract_binding"]["scheduler_execution_args"] + from loopx.control_plane.quota.turn_envelope import _turn_envelope_result + + assert ( + _turn_envelope_result( + "compose_complete", payload=denied, scheduler_execution_args=binding + ) + == denied + ) + envelope = build_turn_envelope(denied) + plan = build_loopx_turn_plan( + envelope, + host="codex-cli", + execution_mode="isolated-headless", + turn_instance_id=turn, + ) + assert plan["route"]["would_invoke_host"] is False + if original: + assert denied["heartbeat_receipt"]["status"] == "replayed" + assert ( + denied["heartbeat_receipt"]["event_id"] + == original["heartbeat_receipt"]["event_id"] + ) + assert denied["heartbeat_receipt"].get("settlement_identity") == original[ + "heartbeat_receipt" + ].get("settlement_identity") + else: + assert denied["heartbeat_receipt"]["status"] == "not_committed" + assert _heartbeat_receipt_count(runtime, turn) == receipt_count + assert _spend_run_count(runtime) == 0 and state.read_bytes() == before + + +@pytest.mark.parametrize("flag", [False, True]) +@pytest.mark.parametrize( + "code", + [ + "QUOTA_VALIDATION_FAILED", + "quota_unexpected_collection_error", + "quota_agent_registry_roster_missing", + ], +) +def test_predecision_failure_remains_diagnostic_without_legacy_view(flag, code): + failure = { + "ok": False, + "mode": "should-run", + "decision": "skip", + "should_run": False, + "error_code": code, + "reason": "Original diagnosis", + "recommended_action": "Original recovery", + "diagnostic_evidence": {"unknown_fact": ["preserve", "ordered"]}, + } + original = deepcopy(failure) + result = _project_quota_cli_payload( + failure, + Namespace(quota_command="should-run", turn_envelope=flag), + frozenset(), + None, + ) + assert result == original and failure == original + assert "contract_binding" not in result and "interaction_contract" not in result + with pytest.raises(RuntimeError, match="interaction_contract"): + compose_turn_decision(result) + + +@pytest.mark.parametrize("flag", [False, True]) +@pytest.mark.parametrize("fault", [None, "action", "interaction"]) +def test_unhealthy_paused_decision_keeps_stop_policy_in_complete_capture( + tmp_path, flag, fault +): + from loopx.cli_commands.quota import _emit_quota_result + from loopx.quota import build_quota_should_run + from test_quota_paused_precedence import ( + _paused_status, + _assert_authoritatively_paused, + ) + from test_quota_paused_precedence import ( + AGENT_ID as PAUSED_AGENT, + GOAL_ID as PAUSED_GOAL, + ) + from loopx.control_plane.scheduler.execution_context import ( + GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, + ) + + status = _paused_status() + status["contract"] = {"error_diagnostics": []} + status["global_registry"] = {"ok": False} + source = build_quota_should_run( + status, + goal_id=PAUSED_GOAL, + agent_id=PAUSED_AGENT, + scheduler_execution_context=GENERIC_CLI_OUTER_CONTROLLER_SCHEDULER_CONTEXT, + ) + assert source["ok"] is False and source["status_health_ok"] is False + capture = tmp_path / "capture" + capture.mkdir() + emitted = [] + args = Namespace( + quota_command="should-run", + turn_envelope=flag, + format="json", + agent_id=PAUSED_AGENT, + ) + + def emit(): + return _emit_quota_result( + source, + args, + context=None, + registry_path=tmp_path / "registry.json", + heartbeat_turn_id="paused-unhealthy", + usage_quota_started=0, + capture_directory=capture, + detail_sections=frozenset(), + print_payload=lambda value, *_: emitted.append(value), + ) + + if fault: + if fault == "action": + source["effective_action"] = "future_execution" + else: + source["interaction_contract"]["schema_version"] = "future_interaction" + with pytest.raises( + RuntimeError, match="effective_action|interaction_contract.schema_version" + ): + emit() + assert emitted == [] and not (capture / "decision.json").exists() + return + rc = emit() + assert rc == 1 + result = emitted[0] + _assert_authoritatively_paused(result) + assert result["schema_version"] == "loopx_turn_envelope_v1" + assert result["ok"] is False and result["status_health_ok"] is False + assert result == json.loads((capture / "decision.json").read_text()) + # Complete transport must not discard any health, authority or stop fact. + assert { + k: v + for k, v in result.items() + if k not in {"schema_version", "contract_binding"} + } == source diff --git a/tests/control_plane/test_turn_envelope_capability_facts.py b/tests/control_plane/test_turn_envelope_capability_facts.py index 2dfa922054..2bd0a31c37 100644 --- a/tests/control_plane/test_turn_envelope_capability_facts.py +++ b/tests/control_plane/test_turn_envelope_capability_facts.py @@ -58,14 +58,11 @@ def test_real_captured_guard_preserves_required_and_missing_capabilities( "--turn-envelope", "--decision-output-dir", str(capture), cwd=project) assert code == (0 if network_available else 1), envelope full = json.loads((capture / "decision.json").read_text()) - # Admission failures retain their typed diagnostic/historical projection; - # successful admission emits the complete contract through either spelling. + # Both admission and refusal carry the same complete source. Keep the + # historical decoder checks below without choosing a second producer path. from loopx.control_plane.quota.turn_envelope import build_turn_envelope - if network_available: - assert envelope == full and full["schema_version"] == "loopx_turn_envelope_v1" - envelope = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json")) - else: - assert full.get("schema_version") != "loopx_turn_envelope_v1" + assert envelope == full and full["schema_version"] == "loopx_turn_envelope_v1" + envelope = build_turn_envelope(full, captured_decision_path=str(capture / "decision.json")) gate = full["capability_gate"] assert "network" in gate["required"] assert ("network" in gate["missing"]) is (not network_available) diff --git a/tests/test_turn_envelope.py b/tests/test_turn_envelope.py index 72097d4681..b950538b0f 100644 --- a/tests/test_turn_envelope.py +++ b/tests/test_turn_envelope.py @@ -1007,7 +1007,7 @@ def test_modern_envelope_does_not_bypass_host_signature_check(corruption: str) - @pytest.mark.parametrize("has_packet", [False, True]) -def test_envelope_fallback_preserves_typed_failure_with_or_without_packet(has_packet: bool) -> None: +def test_predecision_diagnostic_preserved_without_historical_envelope(has_packet: bool) -> None: from loopx.cli_commands.quota import _project_quota_cli_payload from argparse import Namespace @@ -1027,7 +1027,7 @@ def test_envelope_fallback_preserves_typed_failure_with_or_without_packet(has_pa assert failure == before return rendered = _project_quota_cli_payload(failure, Namespace(turn_envelope=True, quota_command="should-run"), frozenset(), None) - assert "interaction_contract must be an object" in rendered.pop("turn_envelope_skipped") + assert "turn_envelope_skipped" not in rendered assert rendered == before assert failure == before From cedb06fb4349bfee7efbb5d223a6148e8082aed1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:56:21 +0800 Subject: [PATCH 28/33] docs(controller): record unified refusal and diagnostic boundaries Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 28 +++++++++++++++++-- ...ller-executor-unified-contract-v0.zh-CN.md | 22 +++++++++++++-- .../quota-cli-hot-path-compaction-v0.md | 14 ++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 0937c33ed0..e665f78523 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -833,8 +833,32 @@ Captured original entry/review/writeback total 23,658 versus 23,416 fused bytes. The no-audit control remains 20,543 versus 21,738 non-staged bytes. These are not tokens/cache, latency, extra reads, framing or accepted outcomes. Restored sharing and fewer rule owners do not prove positive full-cost benefit. The legacy audit -delegate retires with its display callers; rejected admission, remaining consumer -retirement and packaged/outcome qualification still keep C1 open. +delegate retires with its display callers; remaining consumer retirement and +packaged/outcome qualification still keep C1 open. + +Rejected admission now uses the same complete composer before immutable capture +when the decision has an interaction contract, including an unhealthy paused +source. A false `ok` still prevents host handoff and native context-baseline +publication and retains the failing exit status. Composition grants no delivery +or settlement authority. Invalid complete sources fail validation before capture. +Errors before a work contract exists retain their original typed diagnostic and +recovery facts, with either CLI flag spelling; they do not acquire a manufactured +work contract or pass through the historical envelope renderer. + +Isolated real File/SQLite CLI checks cover missing work, unavailable capability +and selection-required reentry under both App-heartbeat and generic profiles. +They preserve the original receipt on reentry and create no spend, baseline or +unauthorized state mutation. Capture consumers are being moved to exact complete +readback while keeping explicit historical long-route recovery coverage. This +qualifies local transport and refusal semantics, not installed App adoption or +all-provider/full-path delivery. Remaining consumer retirement, cache consistency +and packaged single-session outcome/cost qualification still keep C1 open. + +For an unhealthy-paused synthetic source, the original CLI projection frozen at +`895f38fc5` uses 6,325 compact UTF-8 JSON bytes and the complete contract uses +6,639 (+4.96%). The extra binding preserves common validation; this is no efficiency +win. Tokens/cache, latency, extra reads, framing and accepted outcomes remain +unmeasured for this case. No live model experiment is implied by these checks. Report the field/clause diff and its decision consequence together with actual model input, cached/uncached tokens, added reads/round trips, reconstruction, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index b8c322b953..3efe8585d4 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -620,8 +620,26 @@ vision audit 增量现已将 Python 嵌套精确匹配算法迁入同一 TypeScr 为原 22,337 字节对融合 23,333(+4.46%);capture 原 entry/review/writeback 合计 23,658 对融合 23,416。不含 audit 的控制样例仍为 20,543 对 21,738。它们不计 tokens/cache、延迟、追加读取、请求包装及验收成果。恢复共享、减少规则 owner 仍不 -证明完整成本正向。旧 audit 委托随展示调用者退役;拒绝准入、剩余 consumer 退役及 -打包/成果资格继续使 C1 保持开放。 +证明完整成本正向。旧 audit 委托随展示调用者退役;剩余 consumer 退役及打包/成果 +资格继续使 C1 保持开放。 + +拒绝准入现在只要已有 interaction contract,就在不可变 capture 前使用同一完整 +composer,包括 health 为 false 的暂停来源。`ok=false` 仍阻止 host handoff 和原生 +context baseline 发布,保留失败退出码;组合不授予交付或结算权限。非法完整来源在 +capture 前验证失败。工作契约形成前的错误则在两种 CLI flag 写法下保留原 typed +诊断及恢复事实,不补造工作契约,也不再经过历史 envelope renderer。 + +真实隔离 File/SQLite CLI 覆盖 App heartbeat 和 generic 两种 profile 下的工作 +缺失、能力不可用及 selection-required reentry。重入保留原 receipt,不产生额度 +记账、baseline 或越权状态修改。Capture consumer 正迁向完整原件精确回读,同时 +显式保留历史长路径恢复覆盖。这验收本地传输和拒绝语义,不代表已安装 App 采用或 +全部 provider/入口交付。剩余 consumer 退役、cache 一致性及打包后的单会话成果/ +完整成本资格继续使 C1 保持开放。 + +固定 unhealthy-paused 合成来源在原 CLI projection `895f38fc5` 下为 6,325 个 +紧凑 UTF-8 JSON 字节,完整契约为 6,639(+4.96%)。新增 binding 保留统一验证, +这不是效率收益;该样例的 tokens/cache、延迟、追加读取、请求包装及验收成果仍未 +测量。这些检查不代表启动过真实模型实验。 同时报告字段/条款 diff 及其决策后果、实际模型输入、cached/uncached tokens、追加 读取/往返、重建、延迟、失败/重试、验证与人工介入。首包更小但总补读更多,可能是负向。 diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index 77a8ad2397..e81e6f728d 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -117,6 +117,20 @@ delegate retires with the same caller migration; it is not a second rule owner. 引用链及冲突副本。历史 envelope 保留内联根 judge,旧展示只添加既有 metadata; 委托随同一调用链迁移退役,不构成第二个规则 owner。 +A declined `should-run` decision with an interaction contract uses that same +complete composer before capture. Failure still prevents host handoff and native +context-baseline publication; validation never grants work or settlement. An +error before an interaction contract exists remains the original typed diagnostic +and recovery payload, with either CLI flag spelling. It is neither a fabricated +work contract nor a historical-envelope fallback. Readers must preserve the +original receipt and capture when transport fails after admission. + +已有 interaction contract 的拒绝 `should-run` 判定也在 capture 前使用同一完整 +composer。失败仍阻止 host handoff 和原生 context baseline 发布,验证不授予工作 +或结算权限。interaction contract 形成前的错误在两种 CLI flag 写法下都保留原 +typed 诊断及恢复 payload,不补造工作契约、不回退历史 envelope。准入后传输失败 +时,reader 必须保留原 receipt 和 capture。 + When a replan action carries a complete `vision_authoring` schema, the default `quota should-run` packet now retains it, alongside its executable writeback summary (`required_fields`, accepted path outcomes, and rule). Only when the From e4caf4f22607186f75eab4f28536a4a05de42c03 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:10:58 +0800 Subject: [PATCH 29/33] refactor(quota): share context reference composition and recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/cli_projection.py | 83 ++--------- loopx/control_plane/quota/turn_envelope.ts | 93 ++++++++++++ .../test_quota_context_reference_owner.py | 138 ++++++++++++++++++ .../control_plane/test_replan_staged_read.py | 2 +- 4 files changed, 242 insertions(+), 74 deletions(-) create mode 100644 tests/control_plane/test_quota_context_reference_owner.py diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 3312803abf..e209770013 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -594,47 +594,13 @@ def _deduplicate_agent_lane_next_action( compact.pop("title", None) return compact - # The typed work-context owner has already verified the current full Task. - # A bounded lane label may differ from its full title. Reference only an - # exact duplicate in this same packet, never a summary or a prior capture. - contract = payload.get("interaction_contract") - channel = contract.get("agent_channel") if isinstance(contract, dict) else None - context = channel.get("work_context") if isinstance(channel, dict) else None - selected = payload.get("selected_todo") - if not isinstance(context, dict) or not isinstance(selected, dict): - return item - if (context.get("complete") is not True or context.get("selected_todo_text_verified") is not True - or not isinstance(item.get("todo_id"), str) or not item["todo_id"] or "title_ref" in item - or selected.get("todo_id") != item["todo_id"]): - return item - sources = context.get("sources", []) - if not isinstance(sources, list): - return item - matches = [(index, source.get("content", {})) for index, source in enumerate(sources) - if isinstance(source, dict) and source.get("source") == "selected_todo"] - if len(matches) != 1: - return item - index, content = matches[0] - if not isinstance(content, dict): - return item - todo = content.get("todo", {}) - if (not isinstance(todo, dict) or not isinstance(todo.get("text"), str) - or content.get("ok") is not True or content.get("matched") is not True - or content.get("ambiguous") is True or content.get("read_complete") is False - or todo.get("archive_state") == "archive" or todo.get("status") == "done" - or todo.get("todo_id") != item["todo_id"]): - return item - for field in ("text", "title"): - if todo.get(field) != title: - continue - ref = f"$.interaction_contract.agent_channel.work_context.sources[{index}].content.todo.{field}" - if len(json.dumps(title, ensure_ascii=False)) <= len(ref) + 8: - return item - compact = dict(item) - compact.pop("title") - compact["title_ref"] = ref - return compact - return item + from .turn_envelope import _turn_envelope_result + + # Exact source sharing lives in the existing TypeScript context owner. + # The prefix-based display-only omission above is not a contract rule. + shared = _turn_envelope_result("reference_current_task_title", + payload={**payload, "agent_lane_next_action": item}) + return shared["agent_lane_next_action"] def _compact_goal_route_hint(route: dict[str, Any]) -> dict[str, Any]: @@ -811,39 +777,10 @@ def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any] def _reference_explore_writeback_guidance(payload: dict[str, Any]) -> dict[str, Any]: - """Keep identical optional instructions once in this same CLI packet. + """Display adapter; reuse the TypeScript context reference owner.""" + from .turn_envelope import _turn_envelope_result - This is transport-only reference projection, after all decisions. Preserve - standalone Explore reads, differing text and the complete settlement owner. - """ - contract = payload.get("interaction_contract", {}) - agent = contract.get("agent_channel", {}) - context = agent.get("work_context", {}) - sources = context.get("sources", []) - steps = contract.get("cli_channel", {}).get("settlement_plan", {}).get("ordered_steps", []) - for step_index, step in enumerate(steps): - if step.get("kind") != "durable_writeback": - continue - for attachment_index, attachment in enumerate(step.get("optional_attachments", [])): - guidance = attachment.get("guidance") - if attachment.get("capability_id") != "explore" or not isinstance(guidance, str) or not guidance: - continue - ref = (f"$.interaction_contract.cli_channel.settlement_plan.ordered_steps[{step_index}]" - f".optional_attachments[{attachment_index}].guidance") - updated = list(sources) - for index, source in enumerate(sources): - content = source.get("content", {}) - graph = content.get("graph", {}) - if source.get("kind") != "explore_turn_context" or graph.get("result_writeback_guidance") != guidance: - continue - graph = dict(graph) - graph.pop("result_writeback_guidance") - graph["result_writeback_guidance_ref"] = ref - updated[index] = {**source, "content": {**content, "graph": graph}} - if updated != sources: - return {**payload, "interaction_contract": {**contract, "agent_channel": { - **agent, "work_context": {**context, "sources": updated}}}} - return payload + return _turn_envelope_result("reference_explore_guidance", payload=payload) def compact_quota_should_run_cli_payload( diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 6441f5c62d..d042c0feb3 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -219,6 +219,92 @@ function canonicalHash(value: unknown): string { return `sha256:${createHash("sha256").update(JSON.stringify(canonicalValue(value)), "utf8").digest("hex")}`; } +function resolveExploreGuidance(payload: JsonObject, context: JsonObject): JsonObject { + if (!Array.isArray(context.sources)) return context; + const steps = object(object(object(payload.interaction_contract).cli_channel).settlement_plan).ordered_steps; + return {...context, sources: context.sources.map(value => { + const source = object(value), content = object(source.content), graph = object(content.graph); + if (source.kind !== "explore_turn_context" || !Object.hasOwn(graph, "result_writeback_guidance_ref")) return value; + const reference = graph.result_writeback_guidance_ref; + const match = typeof reference === "string" + ? /^\$\.interaction_contract\.cli_channel\.settlement_plan\.ordered_steps\[(0|[1-9]\d*)\]\.optional_attachments\[(0|[1-9]\d*)\]\.guidance$/.exec(reference) : null; + const step = object(match && Array.isArray(steps) ? steps[Number(match[1])] : null); + const attachment = object(match && Array.isArray(step.optional_attachments) + ? step.optional_attachments[Number(match[2])] : null); + if (Object.hasOwn(graph, "result_writeback_guidance") || step.kind !== "durable_writeback" || + attachment.capability_id !== "explore" || Object.hasOwn(attachment, "guidance_ref") || + typeof attachment.guidance !== "string" || !attachment.guidance) { + throw new EffectRuntimeRequestError("Explore guidance reference must resolve to one inline durable-writeback Explore attachment"); + } + delete graph.result_writeback_guidance_ref; + return {...source, content: {...content, graph: {...graph, result_writeback_guidance: attachment.guidance}}}; + })}; +} + +function referenceExploreGuidance(payload: JsonObject): JsonObject { + const interaction = object(payload.interaction_contract), agent = object(interaction.agent_channel); + const context = object(agent.work_context); + resolveExploreGuidance(payload, context); // Validate existing references, including idempotent reads. + if (!Array.isArray(context.sources)) return payload; + const steps = object(object(interaction.cli_channel).settlement_plan).ordered_steps; + if (!Array.isArray(steps)) return payload; + const owners = new Map(); + steps.forEach((value, stepIndex) => { + const step = object(value); + if (step.kind !== "durable_writeback" || !Array.isArray(step.optional_attachments)) return; + step.optional_attachments.forEach((value, attachmentIndex) => { + const attachment = object(value), guidance = attachment.guidance; + if (attachment.capability_id !== "explore" || Object.hasOwn(attachment, "guidance_ref") || + typeof guidance !== "string" || !guidance || owners.has(guidance)) return; + owners.set(guidance, `$.interaction_contract.cli_channel.settlement_plan.ordered_steps[${stepIndex}].optional_attachments[${attachmentIndex}].guidance`); + }); + }); + const sources = context.sources.map(value => { + const source = object(value), content = object(source.content), graph = object(content.graph); + const guidance = graph.result_writeback_guidance; + const reference = typeof guidance === "string" ? owners.get(guidance) : undefined; + if (source.kind !== "explore_turn_context" || typeof guidance !== "string" || !reference || + Buffer.byteLength(JSON.stringify(guidance), "utf8") <= Buffer.byteLength(reference, "utf8") + 8) return value; + delete graph.result_writeback_guidance; + return {...source, content: {...content, graph: {...graph, result_writeback_guidance_ref: reference}}}; + }); + return {...payload, interaction_contract: {...interaction, agent_channel: {...agent, + work_context: {...context, sources}}}}; +} + +function currentTaskTitleOwner(payload: JsonObject): {index: number; todo: JsonObject} | null { + const lane = object(payload.agent_lane_next_action), selected = object(payload.selected_todo); + const context = object(object(object(payload.interaction_contract).agent_channel).work_context); + if (context.complete !== true || context.selected_todo_text_verified !== true || + typeof lane.todo_id !== "string" || !lane.todo_id || selected.todo_id !== lane.todo_id || + !Array.isArray(context.sources)) return null; + const matches = context.sources.map((value, index) => ({source: object(value), index})) + .filter(({source}) => source.source === "selected_todo"); + if (matches.length !== 1) return null; + const {source, index} = matches[0], content = object(source.content), todo = object(content.todo); + if (content.ok !== true || content.matched !== true || content.ambiguous === true || + content.read_complete === false || typeof todo.text !== "string" || + todo.archive_state === "archive" || todo.status === "done" || todo.todo_id !== lane.todo_id) return null; + return {index, todo}; +} + +function referenceCurrentTaskTitle(payload: JsonObject): JsonObject { + const lane = object(payload.agent_lane_next_action), title = lane.title; + // Preserve historical caller-owned references; complete readers validate them + // separately. Display-only omission of a title already in a label stays local. + if (typeof title !== "string" || Object.hasOwn(lane, "title_ref")) return payload; + const owner = currentTaskTitleOwner(payload); + if (!owner) return payload; + for (const field of ["text", "title"]) { + if (owner.todo[field] !== title || Object.hasOwn(owner.todo, `${field}_ref`)) continue; + const reference = `$.interaction_contract.agent_channel.work_context.sources[${owner.index}].content.todo.${field}`; + if (Array.from(JSON.stringify(title)).length <= reference.length + 8) return payload; + delete lane.title; + return {...payload, agent_lane_next_action: {...lane, title_ref: reference}}; + } + return payload; +} + function compactFields( value: unknown, fields: readonly string[], @@ -355,6 +441,7 @@ function replanGoalContext(payload: JsonObject, reads: JsonObject | null, coreRe * Different revisions, other source fields and unavailable context stay intact. */ function replanWorkContext(payload: JsonObject, context: JsonObject): JsonObject { + context = resolveExploreGuidance(payload, context); const reads = replanReadCommands(payload); const core = object(object(object(payload.autonomous_replan_obligation).replan_context).core_goal); const acceptance = object(core.acceptance_contract); @@ -1272,6 +1359,12 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { assertTurnEnvelopeConsumerVersion(requireJsonObject(request.envelope, "turn_envelope")); return {ok: true}; } + if (operation === "reference_current_task_title") { + return referenceCurrentTaskTitle(requireJsonObject(request.payload, "quota decision")); + } + if (operation === "reference_explore_guidance") { + return referenceExploreGuidance(requireJsonObject(request.payload, "quota decision")); + } if (operation === "build") return buildTurnEnvelope(request); if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request); diff --git a/tests/control_plane/test_quota_context_reference_owner.py b/tests/control_plane/test_quota_context_reference_owner.py new file mode 100644 index 0000000000..011429a1c9 --- /dev/null +++ b/tests/control_plane/test_quota_context_reference_owner.py @@ -0,0 +1,138 @@ +"""Shared context references preserve full instructions across existing entrypoints.""" +from copy import deepcopy +import hashlib +import json + +import pytest + +from loopx.control_plane.quota.cli_projection import _reference_explore_writeback_guidance +from loopx.control_plane.quota.turn_envelope import _turn_envelope_result, build_turn_envelope +from test_replan_host_context_projection import _quota_payload + + +def packet(): + source = _quota_payload() + guidance = "Keep original requirements, lease ownership, negative evidence and stop conditions. " * 20 + source["interaction_contract"]["agent_channel"]["work_context"] = {"complete": True, "sources": [{ + "kind": "explore_turn_context", "source": "turn_start_capability_hook", + "hook_id": "explore.turn_context", "capability_id": "explore", + "content": {"graph": {"result_writeback_guidance": guidance, + "writeback_results": [{"status": "refuted", "evidence": "fixture:negative"}]}}}]} + source["interaction_contract"]["cli_channel"]["settlement_plan"] = { + "identity": {"turn_instance_id": "original-turn"}, "ordered_steps": [ + {"kind": "validation"}, {"kind": "durable_writeback", "optional_attachments": [ + {"capability_id": "explore", "guidance": guidance, "required": False}]}]} + return source, guidance + + +def graph(source): + return source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"]["graph"] + + +def attachment(source): + return source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["optional_attachments"][0] + +@pytest.mark.parametrize("captured", [False, True]) +def test_shared_guidance_reaches_existing_readers_without_changing_settlement(captured): + source, guidance = packet() + if captured: + source["replan_read_path"] = "/synthetic/decision.json" + original = deepcopy(source) + shared = _reference_explore_writeback_guidance(source) + assert source == original + assert _reference_explore_writeback_guidance(shared) == shared + assert shared["interaction_contract"]["cli_channel"] == source["interaction_contract"]["cli_channel"] + assert graph(shared)["writeback_results"] == graph(source)["writeback_results"] + assert "result_writeback_guidance" not in graph(shared) + envelope = build_turn_envelope(shared) + assert envelope["schema_version"] == "loopx_turn_envelope_v0" + assert envelope["work_context"]["sources"][0]["content"]["graph"] == graph(source) + assert envelope["action_signature"]["matches"] is True + assert graph(source)["result_writeback_guidance"] == guidance + if captured: + projected = _turn_envelope_result("replan_cli_view", payload=shared) + assert graph(projected) == graph(source) + digest = "sha256:" + hashlib.sha256(json.dumps(shared, ensure_ascii=False, + sort_keys=True, separators=(",", ":")).encode()).hexdigest() + writer = _turn_envelope_result("replan_view", payload=shared, source_hash=digest, stage="writeback") + assert writer["writeback"]["settlement_plan"] == source["interaction_contract"]["cli_channel"]["settlement_plan"] + + +@pytest.mark.parametrize("mutation", ["different", "wrong-step", "wrong-capability", "missing-owner", "short", "wrong-source"]) +def test_nonidentical_or_unavailable_guidance_stays_inline(mutation): + source, _ = packet() + if mutation == "different": + attachment(source)["guidance"] += " Extra stop." + elif mutation == "wrong-step": + source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["kind"] = "validation" + elif mutation == "wrong-capability": + attachment(source)["capability_id"] = "another" + elif mutation == "missing-owner": + attachment(source).pop("guidance") + elif mutation == "short": + attachment(source)["guidance"] = graph(source)["result_writeback_guidance"] = "Keep evidence." + else: + source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["kind"] = "other_context" + assert graph(_reference_explore_writeback_guidance(source)) == graph(source) + + +@pytest.mark.parametrize("mutation", ["dangling", "wrong-step", "wrong-capability", "chain", "both", "malformed"]) +def test_invalid_reference_is_rejected_before_relocation(mutation): + source, _ = packet() + source["replan_read_path"] = "/synthetic/decision.json" + shared = _reference_explore_writeback_guidance(source) + if mutation == "dangling": + graph(shared)["result_writeback_guidance_ref"] = graph(shared)["result_writeback_guidance_ref"].replace("[1]", "[99]") + elif mutation == "wrong-step": + shared["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][1]["kind"] = "validation" + elif mutation == "wrong-capability": + attachment(shared)["capability_id"] = "another" + elif mutation == "chain": + attachment(shared)["guidance_ref"] = "$.unknown" + elif mutation == "both": + graph(shared)["result_writeback_guidance"] = "Conflicting instruction." + else: + graph(shared)["result_writeback_guidance_ref"] = {"path": "$.unknown"} + for read in (lambda: _reference_explore_writeback_guidance(shared), + lambda: build_turn_envelope(shared), + lambda: _turn_envelope_result("replan_cli_view", payload=shared)): + with pytest.raises(RuntimeError, match="Explore guidance reference"): + read() + + +@pytest.mark.parametrize("body", ["🙂" * 90, "🙂" * 140, "完整要求\n" * 100]) +def test_title_threshold_preserves_unicode_character_semantics(body): + from loopx.control_plane.quota.cli_projection import _deduplicate_agent_lane_next_action + path = "$.interaction_contract.agent_channel.work_context.sources[0].content.todo.text" + lane = {"todo_id": "task-one", "text": "Bounded label", "title": body} + source = {"selected_todo": {"todo_id": "task-one"}, "agent_lane_next_action": lane, + "interaction_contract": {"agent_channel": {"work_context": { + "complete": True, "selected_todo_text_verified": True, "sources": [{ + "source": "selected_todo", "content": {"ok": True, "matched": True, + "todo": {"todo_id": "task-one", "status": "open", "text": body}}}]}}}} + original = deepcopy(source) + shared = _deduplicate_agent_lane_next_action(lane, payload=source) + if len(json.dumps(body, ensure_ascii=False)) > len(path) + 8: + assert shared == {"todo_id": "task-one", "text": "Bounded label", "title_ref": path} + else: + assert shared == lane + assert source == original + + +def test_distinct_guidance_owners_restore_each_original_source(): + source, first = packet() + context = source["interaction_contract"]["agent_channel"]["work_context"] + second = "Retain this separate capability evidence and its original stop conditions. " * 20 + another = deepcopy(context["sources"][0]) + another["content"]["graph"]["result_writeback_guidance"] = second + context["sources"].append(another) + steps = source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] + steps[1]["optional_attachments"].append({"capability_id": "explore", "guidance": second}) + original = deepcopy(source) + shared = _reference_explore_writeback_guidance(source) + assert source == original + graphs = [item["content"]["graph"] for item in shared["interaction_contract"]["agent_channel"]["work_context"]["sources"]] + assert graphs[0]["result_writeback_guidance_ref"].endswith("optional_attachments[0].guidance") + assert graphs[1]["result_writeback_guidance_ref"].endswith("optional_attachments[1].guidance") + restored = build_turn_envelope(shared)["work_context"]["sources"] + assert [item["content"]["graph"]["result_writeback_guidance"] for item in restored] == [first, second] diff --git a/tests/control_plane/test_replan_staged_read.py b/tests/control_plane/test_replan_staged_read.py index d6c5cc2078..780a14138c 100644 --- a/tests/control_plane/test_replan_staged_read.py +++ b/tests/control_plane/test_replan_staged_read.py @@ -129,7 +129,7 @@ def test_pure_replan_does_not_advertise_unavailable_explore_attachment(tmp_path, # not suggest a new way around it or change its primary commit behavior. attachment = {**original["result_attachment_template"], "question": "Is the source valid?", "applicability": "Synthetic source", "input_revision": "source-v1", "observation": "Reviewed", - "interpretation": "Unresolved", "evidence_refs": ["evidence:source-review"]} + "interpretation": "Unresolved", "status": "tentative", "evidence_refs": ["evidence:source-review"]} with pytest.raises(ValueError, match="requires agent, Todo and Turn identity"): prepare_result_attachment(attachment, registry_path=tmp_path / "registry.json", runtime_root=runtime, goal_id=goal, agent_id=agent, todo_id=None, turn_instance_id=turn) From 2f40c7c8a0ccdfe61b539eb7ad82d781e882493f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:11:04 +0800 Subject: [PATCH 30/33] docs(controller): separate prerequisite PRs from contract cutover Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../controller-executor-unified-contract-v0.md | 15 +++++++++++++-- ...oller-executor-unified-contract-v0.zh-CN.md | 10 +++++++++- .../quota-cli-hot-path-compaction-v0.md | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 4c9b6074ab..3c12434233 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -735,8 +735,19 @@ Implementation PRs must name the replaced caller, remaining gap, actual tests and rollback. No full language migration or unrelated manager rewrite is a prerequisite. No task is complete merely because a new abstraction compiles. -For C1, the first implementation slice pairs the complete field/clause migration -with a real single-session producer and consumer replacement. Start at the +C1 is one acceptance target, not one mandatory PR. Independently useful +prerequisites may land first when existing callers adopt the shared owner, their +entrypoint contracts are qualified, and rollback is local. For example, move +exact context-reference composition and recovery into the existing TypeScript +owner while the current quota and Turn entries keep their schemas. Name the +remaining cutover consumer; do not add unused infrastructure or a second ledger. +Separate shared-owner prerequisites, producer/capture cutover, reader recovery +and retirement, and adoption/full-cost qualification where each boundary is +independently reviewable. Record dependencies in the existing tasks and PRs. +Splitting delivery does not close C1 or waive §9's original quota-baseline test. + +The first **cutover** slice pairs the complete field/clause migration with a +real single-session producer and consumer replacement. Start at the shared quota emission boundary and trace both attached heartbeat delivery and the Turn adapter; retain their actual host ownership. Do not build an unused third serializer while both active writers keep their own semantics. Existing diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 1a201c51f5..55d130c7dd 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -559,7 +559,15 @@ C1 盘点是可用替换的一部分,不单独建设架构框架。C1 契约 缺口、实际测试与回退。完整语言迁移或无关 manager 重写不是前置条件;新抽象能编译 不能算任务完成。 -C1 首个实现片必须把完整字段/条款迁移与真实单会话 producer、consumer 替换一起交付。 +C1 是一个验收目标,不要求放进一个 PR。独立有用的前置改动可以先落地,前提是现有 +调用方实际采用共享 owner、入口契约经过验证,且能局部回退。例如,在现有 quota 和 +Turn 入口保持各自 schema 时,将精确上下文引用组合与恢复收敛到既有 TypeScript +owner。列明后续切换消费者;不能添加无人使用的基础设施或第二套台账。 +共享 owner 前置、producer/capture 切换、reader 恢复与退役、采用及完整成本验收, +可按独立可审查的边界拆分,在既有任务和 PR 中记录依赖。拆分交付不代表 C1 完成, +也不豁免第 9 节相对原 quota 基线的验证。 + +首个**切换**片必须把完整字段/条款迁移与真实单会话 producer、consumer 替换一起交付。 从共享 quota emission 边界开始,同时追踪 attached heartbeat 交付与 Turn adapter, 保留各自真实宿主归属。不能新增无人使用的第三个 serializer,而两个活跃 writer 继续 各自拥有语义。既有 `test_quota_decision_capture.py` 与 diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index daf54fa304..6680f32413 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -85,6 +85,24 @@ neither enables native tracing nor changes the default-off transport switch. 控制字段和原始捕获均保留。`--include-detail agent-todos` 保留内联标题。 这是普通 CLI 序列化去重,差分开关关闭和开启时均适用,不会开启原生追踪或改变默认关闭。 +Exact Task-title and Explore-guidance sharing now use the existing TypeScript +context owner; the Python quota projection delegates those operations. Current +quota and Turn entrypoints retain their schemas. Explore guidance shares only +identical inline durable-writeback instructions within the same response, and +stays inline when the reference would be larger. Before relocating context, +Turn and staged replan readers restore that guidance from its settlement owner. +Dangling, chained, conflicting or incorrectly typed references reject rather +than silently losing instructions. Settlement commands, identities and original +source content remain unchanged. This shared-owner prerequisite does not qualify +the proposed complete-contract cutover or claim an end-to-end efficiency gain. + +精确任务标题与 Explore 指引共享复用既有 TypeScript 上下文 owner,Python quota +投影委托这两项操作;当前 quota 与 Turn 入口保持各自 schema。Explore 仅共享本响应 +内与 durable-writeback 指引完全相同的正文;引用更大时保留内联。Turn 与分阶段 +replan reader 在移动上下文前,从原结算 owner 恢复指引。悬空、链式、冲突或类型 +错误的引用会被拒绝,不静默丢失指令。结算命令、身份与原始源内容保持不变。 +这一共享 owner 前置不代表完整契约切换已通过验收,也不声称端到端效率提升。 + When a replan action carries a complete `vision_authoring` schema, the default `quota should-run` packet now retains it, alongside its executable writeback summary (`required_fields`, accepted path outcomes, and rule). Only when the From 56559929aad42dc86df8b0035092595a48ba0ac8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:31:11 +0800 Subject: [PATCH 31/33] perf(quota): combine shared context projection transport Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../quota-cli-hot-path-compaction-v0.md | 8 ++- loopx/control_plane/quota/cli_projection.py | 40 ++++++------ loopx/control_plane/quota/turn_envelope.ts | 10 +-- .../test_quota_context_reference_owner.py | 61 ++++++++++++++++--- 4 files changed, 83 insertions(+), 36 deletions(-) diff --git a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md index 6680f32413..ae2ec08b9b 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -86,7 +86,9 @@ neither enables native tracing nor changes the default-off transport switch. 这是普通 CLI 序列化去重,差分开关关闭和开启时均适用,不会开启原生追踪或改变默认关闭。 Exact Task-title and Explore-guidance sharing now use the existing TypeScript -context owner; the Python quota projection delegates those operations. Current +context owner; the Python quota projection delegates both operations in one +coarse transport call, preserving each explicit detail selector. With both +selectors enabled, or no context source carrier, it needs no sharing call. Current quota and Turn entrypoints retain their schemas. Explore guidance shares only identical inline durable-writeback instructions within the same response, and stays inline when the reference would be larger. Before relocating context, @@ -97,7 +99,9 @@ source content remain unchanged. This shared-owner prerequisite does not qualify the proposed complete-contract cutover or claim an end-to-end efficiency gain. 精确任务标题与 Explore 指引共享复用既有 TypeScript 上下文 owner,Python quota -投影委托这两项操作;当前 quota 与 Turn 入口保持各自 schema。Explore 仅共享本响应 +投影通过一次粗粒度调用委托这两项操作,分别保留显式 detail 选择;两项 detail +均启用或不存在上下文源载体时无需共享调用。当前 quota 与 Turn 入口保持各自 +schema。Explore 仅共享本响应 内与 durable-writeback 指引完全相同的正文;引用更大时保留内联。Turn 与分阶段 replan reader 在移动上下文前,从原结算 owner 恢复指引。悬空、链式、冲突或类型 错误的引用会被拒绝,不静默丢失指令。结算命令、身份与原始源内容保持不变。 diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index e209770013..58c8868719 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -582,25 +582,15 @@ def _deduplicate_next_action_warning( return compact -def _deduplicate_agent_lane_next_action( - item: dict[str, Any], *, payload: dict[str, Any], -) -> dict[str, Any]: +def _deduplicate_agent_lane_next_action(item: dict[str, Any]) -> dict[str, Any]: + # Display-only omission stays local; exact source sharing runs once below. text = item.get("text") title = item.get("title") - if not isinstance(title, str): - return item - if isinstance(text, str) and (text == title or text.endswith(f" {title}")): + if isinstance(title, str) and isinstance(text, str) and (text == title or text.endswith(f" {title}")): compact = dict(item) compact.pop("title", None) return compact - - from .turn_envelope import _turn_envelope_result - - # Exact source sharing lives in the existing TypeScript context owner. - # The prefix-based display-only omission above is not a contract rule. - shared = _turn_envelope_result("reference_current_task_title", - payload={**payload, "agent_lane_next_action": item}) - return shared["agent_lane_next_action"] + return item def _compact_goal_route_hint(route: dict[str, Any]) -> dict[str, Any]: @@ -776,11 +766,21 @@ def _reference_inline_goal_acceptance(payload: dict[str, Any]) -> dict[str, Any] return result -def _reference_explore_writeback_guidance(payload: dict[str, Any]) -> dict[str, Any]: - """Display adapter; reuse the TypeScript context reference owner.""" +def _reference_context( + payload: dict[str, Any], *, task_title: bool = True, explore_guidance: bool = True, +) -> dict[str, Any]: + """One coarse transport call for the existing typed context owner.""" + contract = payload.get("interaction_contract") + agent = contract.get("agent_channel") if isinstance(contract, dict) else None + context = agent.get("work_context") if isinstance(agent, dict) else None + # No source carrier means neither operation can act. Leave semantic matching + # and reference validation in TypeScript, rather than replicating its rules. + if not (task_title or explore_guidance) or not isinstance(context, dict) or not context.get("sources"): + return payload from .turn_envelope import _turn_envelope_result - return _turn_envelope_result("reference_explore_guidance", payload=payload) + return _turn_envelope_result("reference_context", payload=payload, + task_title=task_title, explore_guidance=explore_guidance) def compact_quota_should_run_cli_payload( @@ -863,7 +863,7 @@ def compact_quota_should_run_cli_payload( if isinstance(agent_lane_next_action, dict): compact = dict(compact) compact["agent_lane_next_action"] = _deduplicate_agent_lane_next_action( - agent_lane_next_action, payload=compact, + agent_lane_next_action, ) goal_route_hint = payload.get("goal_route_hint") if isinstance(goal_route_hint, dict): @@ -878,8 +878,8 @@ def compact_quota_should_run_cli_payload( agent_lane_next_action.pop("content_revision", None) compact["agent_lane_next_action"] = agent_lane_next_action compact = _reference_inline_goal_acceptance(compact) - if not include_vision_detail: - compact = _reference_explore_writeback_guidance(compact) + compact = _reference_context(compact, task_title=not include_todo_summary_detail, + explore_guidance=not include_vision_detail) return _promote_interaction_contract( _promote_runtime_capability_reentry(compact) ) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index d042c0feb3..74ddc43f40 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1359,11 +1359,11 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { assertTurnEnvelopeConsumerVersion(requireJsonObject(request.envelope, "turn_envelope")); return {ok: true}; } - if (operation === "reference_current_task_title") { - return referenceCurrentTaskTitle(requireJsonObject(request.payload, "quota decision")); - } - if (operation === "reference_explore_guidance") { - return referenceExploreGuidance(requireJsonObject(request.payload, "quota decision")); + if (operation === "reference_context") { + let payload = requireJsonObject(request.payload, "quota decision"); + if (request.task_title === true) payload = referenceCurrentTaskTitle(payload); + if (request.explore_guidance === true) payload = referenceExploreGuidance(payload); + return payload; } if (operation === "build") return buildTurnEnvelope(request); if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); diff --git a/tests/control_plane/test_quota_context_reference_owner.py b/tests/control_plane/test_quota_context_reference_owner.py index 011429a1c9..cd9fbbadec 100644 --- a/tests/control_plane/test_quota_context_reference_owner.py +++ b/tests/control_plane/test_quota_context_reference_owner.py @@ -5,7 +5,7 @@ import pytest -from loopx.control_plane.quota.cli_projection import _reference_explore_writeback_guidance +from loopx.control_plane.quota.cli_projection import _reference_context from loopx.control_plane.quota.turn_envelope import _turn_envelope_result, build_turn_envelope from test_replan_host_context_projection import _quota_payload @@ -38,9 +38,9 @@ def test_shared_guidance_reaches_existing_readers_without_changing_settlement(ca if captured: source["replan_read_path"] = "/synthetic/decision.json" original = deepcopy(source) - shared = _reference_explore_writeback_guidance(source) + shared = _reference_context(source) assert source == original - assert _reference_explore_writeback_guidance(shared) == shared + assert _reference_context(shared) == shared assert shared["interaction_contract"]["cli_channel"] == source["interaction_contract"]["cli_channel"] assert graph(shared)["writeback_results"] == graph(source)["writeback_results"] assert "result_writeback_guidance" not in graph(shared) @@ -73,14 +73,14 @@ def test_nonidentical_or_unavailable_guidance_stays_inline(mutation): attachment(source)["guidance"] = graph(source)["result_writeback_guidance"] = "Keep evidence." else: source["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["kind"] = "other_context" - assert graph(_reference_explore_writeback_guidance(source)) == graph(source) + assert graph(_reference_context(source)) == graph(source) @pytest.mark.parametrize("mutation", ["dangling", "wrong-step", "wrong-capability", "chain", "both", "malformed"]) def test_invalid_reference_is_rejected_before_relocation(mutation): source, _ = packet() source["replan_read_path"] = "/synthetic/decision.json" - shared = _reference_explore_writeback_guidance(source) + shared = _reference_context(source) if mutation == "dangling": graph(shared)["result_writeback_guidance_ref"] = graph(shared)["result_writeback_guidance_ref"].replace("[1]", "[99]") elif mutation == "wrong-step": @@ -93,7 +93,7 @@ def test_invalid_reference_is_rejected_before_relocation(mutation): graph(shared)["result_writeback_guidance"] = "Conflicting instruction." else: graph(shared)["result_writeback_guidance_ref"] = {"path": "$.unknown"} - for read in (lambda: _reference_explore_writeback_guidance(shared), + for read in (lambda: _reference_context(shared), lambda: build_turn_envelope(shared), lambda: _turn_envelope_result("replan_cli_view", payload=shared)): with pytest.raises(RuntimeError, match="Explore guidance reference"): @@ -102,7 +102,6 @@ def test_invalid_reference_is_rejected_before_relocation(mutation): @pytest.mark.parametrize("body", ["🙂" * 90, "🙂" * 140, "完整要求\n" * 100]) def test_title_threshold_preserves_unicode_character_semantics(body): - from loopx.control_plane.quota.cli_projection import _deduplicate_agent_lane_next_action path = "$.interaction_contract.agent_channel.work_context.sources[0].content.todo.text" lane = {"todo_id": "task-one", "text": "Bounded label", "title": body} source = {"selected_todo": {"todo_id": "task-one"}, "agent_lane_next_action": lane, @@ -111,7 +110,7 @@ def test_title_threshold_preserves_unicode_character_semantics(body): "source": "selected_todo", "content": {"ok": True, "matched": True, "todo": {"todo_id": "task-one", "status": "open", "text": body}}}]}}}} original = deepcopy(source) - shared = _deduplicate_agent_lane_next_action(lane, payload=source) + shared = _reference_context(source)["agent_lane_next_action"] if len(json.dumps(body, ensure_ascii=False)) > len(path) + 8: assert shared == {"todo_id": "task-one", "text": "Bounded label", "title_ref": path} else: @@ -129,10 +128,54 @@ def test_distinct_guidance_owners_restore_each_original_source(): steps = source["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"] steps[1]["optional_attachments"].append({"capability_id": "explore", "guidance": second}) original = deepcopy(source) - shared = _reference_explore_writeback_guidance(source) + shared = _reference_context(source) assert source == original graphs = [item["content"]["graph"] for item in shared["interaction_contract"]["agent_channel"]["work_context"]["sources"]] assert graphs[0]["result_writeback_guidance_ref"].endswith("optional_attachments[0].guidance") assert graphs[1]["result_writeback_guidance_ref"].endswith("optional_attachments[1].guidance") restored = build_turn_envelope(shared)["work_context"]["sources"] assert [item["content"]["graph"]["result_writeback_guidance"] for item in restored] == [first, second] + + +@pytest.mark.parametrize("detail", ["none", "task", "vision", "both"]) +def test_context_sharing_uses_one_boundary_and_keeps_explicit_detail(monkeypatch, detail): + from loopx.control_plane.quota.cli_projection import compact_quota_should_run_cli_payload + from loopx.control_plane.quota import turn_envelope + source, guidance = packet() + title = "Retain the complete task requirements and stop conditions. " * 30 + source["selected_todo"] = {"todo_id": "task-one"} + source["agent_lane_next_action"] = {"todo_id": "task-one", "text": "Short label", "title": title} + context = source["interaction_contract"]["agent_channel"]["work_context"] + context["selected_todo_text_verified"] = True + context["sources"].append({"source": "selected_todo", "content": { + "ok": True, "matched": True, "todo": {"todo_id": "task-one", "text": title, "status": "open"}}}) + calls = [] + real = turn_envelope._turn_envelope_result + def observed(operation, **params): + calls.append(operation) + return real(operation, **params) + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", observed) + task_detail, vision_detail = detail in {"task", "both"}, detail in {"vision", "both"} + result = compact_quota_should_run_cli_payload(source, + include_todo_summary_detail=task_detail, include_vision_detail=vision_detail) + assert len(calls) == (0 if detail == "both" else 1) + if task_detail: + assert result["agent_lane_next_action"]["title"] == title + else: + assert result["agent_lane_next_action"]["title_ref"].endswith("sources[1].content.todo.text") + if vision_detail: + assert graph(result)["result_writeback_guidance"] == guidance + else: + assert graph(result)["result_writeback_guidance_ref"].endswith("optional_attachments[0].guidance") + assert result["interaction_contract"]["agent_channel"]["work_context"]["sources"][1]["content"]["todo"]["text"] == title + assert result["interaction_contract"]["cli_channel"] == source["interaction_contract"]["cli_channel"] + + +def test_no_context_sources_needs_no_transport(monkeypatch): + from loopx.control_plane.quota import turn_envelope + def unavailable(*args, **kwargs): + raise AssertionError("no context carrier requires no transport") + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", unavailable) + for source in ({"ok": False, "diagnostic": "unavailable"}, + {"interaction_contract": {"agent_channel": {"work_context": {"sources": []}}}}): + assert _reference_context(source) is source From 1ad16f8adba3c9a837b88cd7ff34ce2601402d44 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:12:56 +0800 Subject: [PATCH 32/33] test(quota): validate live observation clocks in cache parity Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_quota_settlement_cli.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 35c7d697e9..acbe644dda 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -1383,8 +1383,9 @@ def test_scheduler_cache_misses_after_todo_state_changes_without_run( assert "selected_todo" not in refreshed +@pytest.mark.parametrize("include_detail", [False, True]) def test_scheduler_cache_tracks_todo_changes_beyond_display_limit( - tmp_path: Path, + tmp_path: Path, include_detail: bool, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) state_path = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -1407,6 +1408,7 @@ def test_scheduler_cache_tracks_todo_changes_beyond_display_limit( GOAL_ID, "--agent-id", AGENT_ID, + *(["--include-detail", "agent-todos"] if include_detail else []), ) initial_rc, initial = _run_cli( registry_path, @@ -1455,19 +1457,34 @@ def test_scheduler_cache_tracks_todo_changes_beyond_display_limit( assert deferred_rc == 0, deferred assert (index_path.read_bytes() if index_path.exists() else None) == index_before - deferred_cached_rc, deferred_cached = _run_cli( - registry_path, - runtime, - *guard, - "--use-projection-cache", - cwd=project, - ) - deferred_fresh_rc, deferred_fresh = _run_cli( - registry_path, - runtime, - *guard, - cwd=project, - ) + def read_deferred_guard(*cache_args: str) -> tuple[int, dict[str, Any]]: + from copy import deepcopy + + started = datetime.now(timezone.utc).replace(microsecond=0) + rc, payload = _run_cli(registry_path, runtime, *guard, *cache_args, cwd=project) + finished = datetime.now(timezone.utc) + assert rc == 0, payload + comparable = deepcopy(payload) + # The legacy compact view omits item details; its explicit detail + # mode and the complete contract must both retain the observation. + if not include_detail and "deferred_items" not in comparable["agent_todo_summary"]: + return rc, comparable + deferred_items = comparable["agent_todo_summary"]["deferred_items"] + assert len(deferred_items) == 1 + assert deferred_items[0]["todo_id"] == "todo_tail_39" + condition = deferred_items[0]["resume_condition"] + # These are separate live evaluations after a cache miss. Check each + # observation clock against its own invocation, then compare every + # other fact, including the due time, readiness and receipt semantics. + evaluated = datetime.fromisoformat(condition.pop("evaluated_at").replace("Z", "+00:00")) + assert evaluated.tzinfo is not None + assert started <= evaluated <= finished + assert condition["scheduled_for"] == "2099-01-01T00:00:00Z" + assert condition["satisfied"] is False + return rc, comparable + + deferred_cached_rc, deferred_cached = read_deferred_guard("--use-projection-cache") + deferred_fresh_rc, deferred_fresh = read_deferred_guard() assert deferred_cached_rc == deferred_fresh_rc == 0, deferred_cached assert deferred_cached["status_projection_cache"]["hit"] is False assert ( From f9fb959c9440de72c5b0b65fba4f8124c49101c7 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:25:23 +0800 Subject: [PATCH 33/33] fix(turn): validate complete contracts before external worker wakes Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- ...controller-executor-unified-contract-v0.md | 11 ++++ ...ller-executor-unified-contract-v0.zh-CN.md | 8 +++ loopx/control_plane/quota/turn_envelope.py | 5 ++ loopx/control_plane/quota/turn_envelope.ts | 4 ++ scripts/external_scheduler_worker.py | 8 +++ .../test_complete_turn_decision.py | 60 +++++++++++++++++++ 6 files changed, 96 insertions(+) diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 43c6630861..11c59575c7 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -532,6 +532,17 @@ unshipped; the PR is Draft for unfinished author work, not merely pending review established. Historical effect decoding is temporary; it must not become a second permanent executable view. +The external scheduler candidate now validates a versioned complete input through +`assert_complete_turn_decision` and the existing TypeScript complete-reader owner +before reading cadence or wake permission. This is a pure reader: it neither +recomposes the captured host binding nor creates a legacy executor view. Missing +bindings, unsupported versions/required semantics, altered content and invalid +same-capture references fail before the wake command. Unversioned ordinary quota +packets retain their existing compatibility path; retiring that path requires +reconciling supported deployed workers. File/SQLite source-CLI-to-worker tests use +local child markers, not model execution. This qualifies that input boundary, +not attached-host task delivery, settlement or accepted-outcome/full-cost benefit. + ### C1 caller replacement and cutover unit The cutover unit is a supported entrypoint **and its downstream consumers**, diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index 3241eae733..028ca920cd 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -402,6 +402,14 @@ File/SQLite 原生输入检查确认不支持的内层版本在 Codex prompt 执 Draft,不是仅等待评审。尚无模型或成本收益证据。历史 effect 解码只是临时迁移, 不得成为第二份永久可执行视图。 +External scheduler 候选在读取节奏或唤醒许可前,通过 +`assert_complete_turn_decision` 复用现有 TypeScript 完整 reader owner 验证带版本输入。 +这是纯读取:不重组已捕获的 host binding,也不生成旧 executor 视图。缺失 binding、 +不支持的版本/必需语义、内容篡改和无效同 capture 引用均在 wake command 前拒绝。 +无版本普通 quota packet 暂保留原兼容路径,退役须核对受支持的已部署 worker。 +File/SQLite 源码 CLI 到 worker 验证使用本地子进程标记,不执行模型;这里只取得 +该输入边界资格,不代表 attached host 任务交付、结算或验收成果/完整成本收益。 + ### C1 消费链替换与切换单元 切换单元是一个受支持入口**及其下游消费者**,不是一个输出开关。对 diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 7a89a80fe0..bf5a7e5264 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -90,6 +90,11 @@ def compose_turn_decision( return _turn_envelope_result("compose_complete", payload=dict(payload), scheduler_execution_args=args) +def assert_complete_turn_decision(payload: Mapping[str, Any]) -> None: + """Validate the original contract; keep its host binding and caller-owned bytes.""" + _turn_envelope_result("complete_consumer", payload=dict(payload)) + + def build_turn_envelope( payload: Mapping[str, Any], *, diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 558dfd9378..779a45c61d 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1717,6 +1717,10 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { replan_action_packet: replanActionPacket(payload)}; } if (operation === "compose_complete") return composeCompleteTurnDecision(request); + if (operation === "complete_consumer") { + requireCompleteTurnDecision(request.payload); + return {ok: true}; + } if (operation === "validate_complete") { const decision = requireCompleteTurnDecision(request.payload); const envelope = requireJsonObject(request.envelope, "complete Turn executor envelope"); diff --git a/scripts/external_scheduler_worker.py b/scripts/external_scheduler_worker.py index 5f6822e25c..481ff91058 100644 --- a/scripts/external_scheduler_worker.py +++ b/scripts/external_scheduler_worker.py @@ -109,6 +109,14 @@ def _local_scheduler_directive(hint: dict[str, Any]) -> str: def parse_tick(payload: dict[str, Any]) -> TickDecision: + # Versioned source must pass the same semantic/integrity owner as managed + # hosts before scheduler or wake facts are used. Never reinterpret an + # unsupported or damaged complete contract as an unversioned legacy packet. + # Historical ordinary quota packets retain their existing compatibility path. + if "schema_version" in payload or "contract_binding" in payload: + from loopx.control_plane.quota.turn_envelope import assert_complete_turn_decision + + assert_complete_turn_decision(payload) hint = _mapping(payload.get("scheduler_hint")) action = str(hint.get("action") or "").strip() cadence_class = str(hint.get("cadence_class") or "").strip() diff --git a/tests/control_plane/test_complete_turn_decision.py b/tests/control_plane/test_complete_turn_decision.py index d19c38fd2a..bdfe235629 100644 --- a/tests/control_plane/test_complete_turn_decision.py +++ b/tests/control_plane/test_complete_turn_decision.py @@ -651,3 +651,63 @@ def test_real_structured_host_reads_same_complete_replan_source(tmp_path, monkey assert "before reading detail_ref.replan_action_packet.writeback" in prompt assert "outer Turn executor owns those effects" in prompt assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_external_worker_validates_complete_source_before_wake(tmp_path, monkeypatch, provider): + import shlex + from tests.test_external_scheduler_worker import _args, _write_executable, worker + from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, read_fixture_todos + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + state = _configure_read_only_todo(project) + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, handoff_mode="soft_claim", todos=read_fixture_todos(state)), + state_path=state, provider=provider) + rc, contract = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "external-source", + "--runtime-profile", "generic_cli", "--include-detail", "scheduler", + "--available-capability", "shell", "--available-capability", "filesystem_read", cwd=project) + assert rc == 0, contract + assert contract["schema_version"] == "loopx_turn_envelope_v1" + assert contract["interaction_contract"]["agent_channel"]["must_attempt"] is True + original = copy.deepcopy(contract) + before_state = state.read_bytes() + for mutation in ("valid", "version", "missing_version", "missing_binding", "content", + "terminal", "action", "interaction", "reference"): + candidate = copy.deepcopy(contract) + if mutation == "version": + candidate["schema_version"] = "unsupported-future-version" + elif mutation == "missing_version": + candidate.pop("schema_version") + elif mutation == "missing_binding": + candidate.pop("contract_binding") + elif mutation == "content": + candidate["scheduler_hint"]["reason"] = "Changed after capture" + elif mutation == "terminal": + candidate["scheduler_hint"]["unchanged_poll"] = {"local_scheduler": "stop"} + elif mutation == "action": + candidate["effective_action"] = "future_execution" + candidate, _ = _resign_contract(candidate) + elif mutation == "interaction": + candidate["interaction_contract"]["schema_version"] = "future_interaction_version" + candidate, _ = _resign_contract(candidate) + elif mutation == "reference": + candidate.setdefault("agent_lane_next_action", {}).pop("title", None) + candidate["agent_lane_next_action"]["title_ref"] = "$.missing_original_requirement" + candidate, _ = _resign_contract(candidate) + case = tmp_path / mutation + cli, marker = case / "quota", case / "wake-called" + _write_executable(cli, f"#!{sys.executable}\nprint({json.dumps(candidate)!r})\n") + args = _args(fake_cli=cli, state_file=case / "state.json", + wake_cmd=shlex.join([sys.executable, "-c", + f"from pathlib import Path; Path({str(marker)!r}).touch()"]), wake_timeout_seconds=5) + args.goal_id, args.agent_id = GOAL_ID, AGENT_ID + result = worker.run_worker(args) + assert result == (0 if mutation == "valid" else 2), mutation + assert marker.exists() is (mutation == "valid"), mutation + assert contract == original + assert state.read_bytes() == before_state + assert _spend_run_count(runtime) == 0