diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 1593589897..e1a0fd1e6f 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -449,7 +449,7 @@ function nativeContextContinuity(codec: JsonObject): JsonObject { /** Plan and encode one native capture without sending its intermediate view * back through Python. Capture publication and native locking remain Host IO. */ export function prepareProgressiveContextDelivery(request: JsonObject, legacyFields: readonly string[], - decisionFields: readonly string[], plan?: JsonObject): JsonObject { + decisionOrder: InitialContextDecisionOrder, plan?: JsonObject): JsonObject { if (request.include_initial_read_plan !== undefined) requireBoolean(request.include_initial_read_plan, "include_initial_read_plan"); const sourceValues: ProjectionSourceValues = new WeakMap(); @@ -462,7 +462,7 @@ export function prepareProgressiveContextDelivery(request: JsonObject, legacyFie if (request.source_edits === true && delivery.mode === "full") delete delivery.context; const observe = request.observe_first_read === true && !!jsonObject(required.context.selected_todo)?.todo_id; const groups = observe || request.include_initial_read_plan === true - ? initialContextReadGroups(required.context, decisionFields) : undefined; + ? initialContextReadGroups(required.context, decisionOrder) : undefined; const pointers = groups ? [...groups.opening, ...groups.remaining] : []; const observed = observe && pointers.length ? observedFirstRead(codec, pointers) : undefined; const {operation: _operation, source_edits: _edits, source_number_tokens: _numbers, ...source} = request; @@ -493,12 +493,17 @@ function nativeContextReadPlan(context: JsonObject, pointers: string[], ? {opening: select(groups.opening), remaining: select(groups.remaining)} : {})}; } -export function initialContextReadPointers(context: JsonObject, decisionFields: readonly string[]): string[] { - const {opening, remaining} = initialContextReadGroups(context, decisionFields); +interface InitialContextDecisionOrder { + fields: readonly string[]; + afterReadyInteraction?: string; +} + +export function initialContextReadPointers(context: JsonObject, order: InitialContextDecisionOrder): string[] { + const {opening, remaining} = initialContextReadGroups(context, order); return [...opening, ...remaining]; } -function initialContextReadGroups(context: JsonObject, decisionFields: readonly string[]): { +function initialContextReadGroups(context: JsonObject, {fields, afterReadyInteraction}: InitialContextDecisionOrder): { opening: string[]; remaining: string[]; } { const object = (value: unknown): JsonObject | undefined => @@ -524,7 +529,16 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly && unread.pointers.every(pointer => typeof pointer === "string") && typeof supportingSource?.path === "string" && typeof supportingSource.sha256 === "string" && typeof details?.instruction === "string" && details.instruction.length > 0; - for (const field of decisionFields) include(field); + const channel = object(interaction.agent_channel), cli = object(interaction.cli_channel); + const prepared = object(cli?.execution_preparation); + const ready = context.should_run === true && context.effective_action === EffectiveAction.NORMAL_RUN + && context.normal_delivery_allowed === true && channel?.delivery_allowed === true + && prepared?.state === "ready" && prepared.delivery_allowed === true + && cli?.selection_required !== true && cli?.turn_binding_required !== true; + // In a ready Turn, deliver the bound decision and required reads before + // a potentially long authority block. Keep the entire block in the opening. + const deferredField = ready ? afterReadyInteraction : undefined; + for (const field of fields) if (field !== deferredField) include(field); // A staged replan has already replaced inline acceptance with a pinned // review reader. Deliver its first action and ordering without writeback. const replan = object(context.replan_action_packet); @@ -560,14 +574,14 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include("interaction_contract", "user_channel"); for (const field of ["primary_action", "delivery_allowed", "must_attempt", "quiet_noop_allowed", "next_task_action"]) include("interaction_contract", "agent_channel", field); - const turnBindingRequired = object(interaction.cli_channel)?.turn_binding_required === true; + const turnBindingRequired = cli?.turn_binding_required === true; if (turnBindingRequired) for (const field of ["turn_binding_required", "next_cli_actions", "spend_allowed_now", "spend_after_validation", "spend_policy"]) include("interaction_contract", "cli_channel", field); include("interaction_contract", "agent_channel", "required_reads"); const work = ["interaction_contract", "agent_channel", "work_context"]; include(...work, "instruction"); - const channel = object(interaction.agent_channel); + if (deferredField && fields.includes(deferredField)) include(deferredField); const sources = object(channel?.work_context)?.sources; const sourceRows = Array.isArray(sources) ? sources.map((source, index) => ({source, index})) : []; const selectedTodo = object(context.selected_todo); @@ -683,13 +697,11 @@ function initialContextReadGroups(context: JsonObject, decisionFields: readonly include(...source, "content", "harness", "guidance"); include(...source, "content", "harness", "orchestration_gate"); }); - const cli = object(interaction.cli_channel); const planPath = ["interaction_contract", "cli_channel", "settlement_plan"]; const plan = object(cli?.settlement_plan); const steps = (plan?.schema_version === "quota_settlement_plan_v1" || plan?.schema_version === "quota_settlement_plan_v2") && Array.isArray(plan.ordered_steps) ? plan.ordered_steps : undefined; - const prepared = object(cli?.execution_preparation); const identity = object(plan?.identity); const settlementSteps = steps?.map(object).filter(step => step?.kind === "durable_writeback" || step?.kind === "quota_spend"); // A ready Turn already shows its ordered writeback and spend with their diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 649ffaad59..c373d94c44 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -2214,6 +2214,7 @@ const INITIAL_CONTEXT_DECISION_FIELDS = ["ok", "schema_version", "goal_id", "age "normal_delivery_allowed", "recovery_delivery_allowed", "self_repair_allowed", "capability_repair_allowed", "workspace_repair_allowed", "safe_bypass_allowed", "safe_bypass_kind", "blocked_action_scope", "reason", "execution_obligation", "goal_boundary", "heartbeat_receipt", "work_lane_contract", "selected_todo"]; +const INITIAL_CONTEXT_DECISION_ORDER = {fields: INITIAL_CONTEXT_DECISION_FIELDS, afterReadyInteraction: "goal_boundary"}; /** The read-only plan needs the current decision and executable authoring * guidance. Evidence and diagnostics remain in its exact source capture. */ @@ -2259,7 +2260,7 @@ function capturedDecisionSelection(request: JsonObject): JsonObject { const initial = request.initial_read === true || request.list_fields === true && Array.isArray(request.select) && request.select.length === 1 && request.select[0] === ""; return capturedContextSelection(request, initial ? initialContextReadPointers( - requireJsonObject(request.context, "captured context"), INITIAL_CONTEXT_DECISION_FIELDS) : []); + requireJsonObject(request.context, "captured context"), INITIAL_CONTEXT_DECISION_ORDER) : []); } function replanCliView(payload: JsonObject, identity: ReplanValueIdentity = canonicalHash, @@ -2507,7 +2508,7 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { return prepareProgressiveContext(request, checkedContextPresentationPlan(request)); if (operation === "context_delivery") return request.native_projection === undefined ? prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS) - : prepareProgressiveContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS, INITIAL_CONTEXT_DECISION_FIELDS, + : prepareProgressiveContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS, INITIAL_CONTEXT_DECISION_ORDER, checkedContextPresentationPlan(request)); if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request); if (operation === "context_recovery") return restoreContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index 915e4ceaef..ff34025bc8 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -456,7 +456,10 @@ def test_cli_current_goal_and_action_precede_complete_scoped_preferences(tmp_pat @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_advertised_native_reader_keeps_the_complete_current_contract_across_pages(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("wide_boundary", [False, True]) +def test_advertised_native_reader_keeps_the_complete_current_contract_across_pages( + tmp_path, monkeypatch, provider, wide_boundary, +): from test_quota_settlement_cli import ( AGENT_ID, GOAL_ID, TODO_ID, _heartbeat_receipt_count, _initialize_fixture_authority, _run_cli, _spend_run_count, _write_fixture, @@ -467,6 +470,11 @@ def test_advertised_native_reader_keeps_the_complete_current_contract_across_pag isolate_sqlite_runtime(tmp_path, monkeypatch) monkeypatch.delenv("CODEX_THREAD_ID", raising=False) project, runtime, registry = _write_fixture(tmp_path) + if wide_boundary: + scopes = [f"src/feature-{index:03}/nested/acceptance/canonical-output/**" for index in range(270)] + registry_body = json.loads(registry.read_text()) + registry_body["goals"][0]["coordination"]["write_scope"] = scopes + registry.write_text(json.dumps(registry_body)) _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) @@ -481,21 +489,34 @@ def test_advertised_native_reader_keeps_the_complete_current_contract_across_pag _, reference = read_context_read_reference(view["recovery"]["reference_file"]) raw = Path(reference["path"]).read_bytes() source = json.loads(raw) + page_limit = 12000 if wide_boundary else 4500 words = shlex.split(view["first_read_command"]) for option in ("--page-bytes", "--max-page-output-bytes"): - words[words.index(option) + 1] = "4500" + words[words.index(option) + 1] = str(page_limit) content = [] for _ in range(20): code, page = _run_cli(registry, runtime, *words[words.index("quota"):], cwd=project) assert code == 0 and page["ok"], page - assert len((json.dumps(page, ensure_ascii=False, separators=(",", ":")) + "\n").encode()) <= 4500 + assert len((json.dumps(page, ensure_ascii=False, separators=(",", ":")) + "\n").encode()) <= page_limit assert capture_page_range(page, raw=raw, digest=reference["sha256"]) is None content.append(page["content"]) if page["eof"]: break words = shlex.split(page["next_command"]) assert page["eof"] and len(content) > 1 - values = selected_context_values("".join(content)) + selection = "".join(content) + values = selected_context_values(selection) + if wide_boundary: + assert source["execution_preparation"]["state"] == "ready" + boundary = source["goal_boundary"] + assert set(scopes).issubset(boundary["write_scope"]) + assert len(json.dumps(boundary).encode()) > page_limit + assert values["/goal_boundary"] == boundary + rows = json.loads(selection)["selections"] + for field in ("/heartbeat_receipt", "/selected_todo", "/interaction_contract/agent_channel/required_reads"): + row = next(row for row in rows + if field == row["pointer"] or field.startswith(row["pointer"] + "/")) + assert json.dumps(row, ensure_ascii=False, separators=(",", ":")) in content[0] cli_path = "/interaction_contract/cli_channel" expected_cli = selected_context_values(json.dumps({"selections": [ {"pointer": cli_path, "value": source["interaction_contract"]["cli_channel"]}]})) diff --git a/tests/control_plane_ts/progressive_context.test.ts b/tests/control_plane_ts/progressive_context.test.ts index cb63c9d2aa..00f266983c 100644 --- a/tests/control_plane_ts/progressive_context.test.ts +++ b/tests/control_plane_ts/progressive_context.test.ts @@ -4,6 +4,8 @@ import {visionAuthoringContract} from "../../loopx/control_plane/goals/vision_ch 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"; +const noDecisionFields = {fields: []}; + const outcome = {id: "o", objective: "Keep all requirements.", revision: 1, owner_agent_id: "a", todo_ids: ["t"], sources: [{ref: "original", summary: "Supporting history."}], requirements: [{id: "r", description: "Final stop.", status: "unverified", planning_satisfied: false, agent_review: {id: "r", conclusion: "unmet", note: "Why it failed.", @@ -22,15 +24,15 @@ test("initial navigation keeps unknown supporting-index obligations whole", () = const context = {interaction_contract: {schema_version: "loopx_interaction_contract_v0"}, context_details: {instruction: "Read applicable supporting evidence before dependent work.", unread_context_index: index}}; const before = structuredClone(context); - assert.ok(initialContextReadPointers(context, []).includes(base)); + assert.ok(initialContextReadPointers(context, noDecisionFields).includes(base)); assert.deepEqual(context, before); } assert.ok(initialContextReadPointers({interaction_contract: {schema_version: "loopx_interaction_contract_v0"}, - context_details: {unread_context_index: known}}, []).includes(base)); - assert.deepEqual(initialContextReadPointers({interaction_contract: {schema_version: "loopx_interaction_contract_v0"}}, []), []); + context_details: {unread_context_index: known}}, noDecisionFields).includes(base)); + assert.deepEqual(initialContextReadPointers({interaction_contract: {schema_version: "loopx_interaction_contract_v0"}}, noDecisionFields), []); for (const index of [known, {...known, source_blocks: ["/supporting/source/content"]}]) { const pointers = initialContextReadPointers({interaction_contract: {schema_version: "loopx_interaction_contract_v0"}, - context_details: {instruction: "Read applicable evidence before dependent work.", unread_context_index: index}}, []); + context_details: {instruction: "Read applicable evidence before dependent work.", unread_context_index: index}}, noDecisionFields); assert.ok(pointers.includes(`${base}/source`)); assert.ok(pointers.includes(`${base}/read_before_dependent_work`)); assert.ok(pointers.includes("/context_details/instruction")); @@ -53,7 +55,7 @@ test("initial outcome ids carry only verified supporting-material addresses", () source: {path: "complete.json", sha256: "fixture-digest"}}}}; const canonicalAlias = base + "/0/supporting_material_pointer"; const before = structuredClone(context); - const first = initialContextReadPointers(context, []); + const first = initialContextReadPointers(context, noDecisionFields); assert.ok(first.includes(base + "/0/id") && !first.includes(canonicalAlias)); assert.ok(first.includes(base + "/1/supporting_material_pointer"), "preserve an explicit different address"); assert.ok(first.includes(base + "/2/supporting_material_pointer"), "a missing id cannot carry the address"); @@ -63,7 +65,7 @@ test("initial outcome ids carry only verified supporting-material addresses", () {...context.context_details, path: "different.json"}, {...context.context_details, sha256: "different-digest"}, {...context.context_details, instruction: undefined}, {...context.context_details, unread_context_index: {...context.context_details.unread_context_index, future_obligation: true}}]) { - assert.ok(initialContextReadPointers({...context, context_details: changed}, []).includes(canonicalAlias)); + assert.ok(initialContextReadPointers({...context, context_details: changed}, noDecisionFields).includes(canonicalAlias)); } }); @@ -171,11 +173,11 @@ test("first read keeps decision rules and preserves nonstandard Vision obligatio 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, []); + const pointers = initialContextReadPointers(context, noDecisionFields); 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)); + assert.ok(initialContextReadPointers(context, noDecisionFields).includes(base)); } });