diff --git a/examples/project/goal-vision-refresh-state-budget-smoke.py b/examples/project/goal-vision-refresh-state-budget-smoke.py index 2c5e5238e5..63d9ed62ab 100644 --- a/examples/project/goal-vision-refresh-state-budget-smoke.py +++ b/examples/project/goal-vision-refresh-state-budget-smoke.py @@ -175,6 +175,8 @@ def run_quota(registry_path: Path, runtime: Path) -> dict: "json", "quota", "should-run", + "--context-delivery", + "off", "--goal-id", GOAL_ID, "--agent-id", diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 37a31d584f..b80e528713 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -449,6 +449,9 @@ def fits() -> bool: if initial_read_pointers: overview = result.pop("overview") instruction = result["instruction"] + if any(pointer.endswith("/vision_authoring/rule") for pointer in initial_read_pointers): + instruction = ("Before composing a Vision, select the complete vision_authoring object " + "at the displayed step in this capture. " + instruction) result["instruction"] = ("Start with first_read_command for current facts and source navigation from this capture, " "without reading an older baseline. " "Read each complete optional attachment from its captured step before using it. " + instruction) diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index b0ca41608a..6a74ed8a72 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -1,5 +1,7 @@ /** Native model presentation: requirements stay inline, supporting material is * addressed in the immutable source. This plan changes no canonical state. */ +import {isDeepStrictEqual} from "node:util"; +import {visionAuthoringContract} from "../goals/vision_checkpoint.ts"; import type {JsonObject} from "../effect_program.ts"; import {jsonObject, requireJsonObject} from "../runtime_decode.ts"; import {GOAL_OUTCOMES_SCHEMA, OUTCOME_CONTEXT_NAVIGATION_SCHEMA} from "../goals/outcome_requirements.ts"; @@ -312,6 +314,13 @@ export function initialContextReadPointers(context: JsonObject, decisionFields: const step = object(value), stepPath = [...planPath, "ordered_steps", stepIndex]; if (!step) { include(...stepPath); return; } for (const field of Object.keys(step)) { + if (field === "vision_authoring" && step.kind === "durable_writeback" + && [false, true].some(replan => isDeepStrictEqual(step[field], visionAuthoringContract(replan)))) { + // Keep the decision and continuation policy at admission. Field limits + // and worked examples are read from this step when writing the Vision. + for (const key of ["schema_version", "rule", "continuation_review"]) include(...stepPath, field, key); + continue; + } if (field !== "optional_attachments" || !Array.isArray(step[field])) { include(...stepPath, field); continue; } step[field].forEach((attachment, attachmentIndex) => { const path = [...stepPath, field, attachmentIndex]; diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index d70573033f..3e32980ec0 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -270,6 +270,9 @@ def test_advertised_native_reader_keeps_the_complete_current_contract_across_pag monkeypatch.delenv("CODEX_THREAD_ID", raising=False) project, runtime, registry = _write_fixture(tmp_path) _initialize_fixture_authority(project, runtime, provider=provider, handoff_mode="hard_lease") + code, claim = _run_cli(registry, runtime, "todo", "claim", "--goal-id", GOAL_ID, + "--todo-id", TODO_ID, "--agent-id", AGENT_ID, "--claimed-by", AGENT_ID, cwd=project) + assert code == 0 and claim["ok"], claim state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" original_state = state.read_bytes() turn_id = "native-first-read" @@ -295,7 +298,21 @@ def test_advertised_native_reader_keeps_the_complete_current_contract_across_pag words = shlex.split(page["next_command"]) assert page["eof"] and len(content) > 1 values = selected_context_values("".join(content)) - assert values["/interaction_contract/cli_channel"] == source["interaction_contract"]["cli_channel"] + cli_path = "/interaction_contract/cli_channel" + expected_cli = selected_context_values(json.dumps({"selections": [ + {"pointer": cli_path, "value": source["interaction_contract"]["cli_channel"]}]})) + vision_path = next(path for path in expected_cli if path.endswith("/vision_authoring")) + # Every CLI leaf outside the authoring reference must still be delivered. + for path, item in expected_cli.items(): + if path.startswith(vision_path + "/") or (isinstance(item, (dict, list)) and item): + continue + assert values[path] == item + assert values[vision_path + "/rule"] == expected_cli[vision_path]["rule"] + code, detail = _run_cli(registry, runtime, "quota", "context-read", "--reference-file", + view["recovery"]["reference_file"], "--select", vision_path, "--page-bytes", "8000", + "--max-page-output-bytes", "9000", cwd=project) + assert code == 0 and detail["eof"] + assert selected_context_values(detail["content"])[vision_path] == expected_cli[vision_path] assert values["/heartbeat_receipt"] == source["heartbeat_receipt"] assert values["/selected_todo"] == source["selected_todo"] assert _heartbeat_receipt_count(runtime, turn_id) == 1 and _spend_run_count(runtime) == 0 diff --git a/tests/control_plane_ts/progressive_context.test.ts b/tests/control_plane_ts/progressive_context.test.ts index de2e6807e3..3013445b4c 100644 --- a/tests/control_plane_ts/progressive_context.test.ts +++ b/tests/control_plane_ts/progressive_context.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; +import {visionAuthoringContract} from "../../loopx/control_plane/goals/vision_checkpoint.ts"; import {initialContextReadPointers, progressiveContextPlan} from "../../loopx/control_plane/quota/progressive_context.ts"; import {FULL_OUTCOME_READ_GUIDANCE, PROGRESSIVE_OUTCOME_READ_GUIDANCE} from "../../loopx/control_plane/work_items/interaction_contract.ts"; @@ -91,3 +92,20 @@ test("exact repeated Todo rows preserve membership without collapsing distinct e assert.equal(progressiveContextPlan({surface: "quota_packet", payload: {...payload, contract_binding: {signature: "signed"}}}).references, undefined); }); + +test("first read keeps decision rules and preserves nonstandard Vision obligations whole", () => { + const base = "/interaction_contract/cli_channel/settlement_plan/ordered_steps/0/vision_authoring"; + for (const replan of [true, false]) { + const schema = visionAuthoringContract(replan); + const required = {kind: "durable_writeback", precondition: "Verify current evidence first.", vision_authoring: schema}; + const context = {interaction_contract: {schema_version: "loopx_interaction_contract_v0", cli_channel: { + settlement_plan: {schema_version: "quota_settlement_plan_v1", ordered_steps: [required]}, + }}}; + const pointers = initialContextReadPointers(context, []); + assert.ok(pointers.includes(base + "/rule")); + assert.equal(pointers.includes(base + "/continuation_review"), Object.hasOwn(schema, "continuation_review")); + assert.ok(!pointers.includes(base)); + required.vision_authoring = {...schema, new_stop: "Keep this changed policy visible."}; + assert.ok(initialContextReadPointers(context, []).includes(base)); + } +});