From dafd69599028cd8f77b459da7d361cb5299edcac Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:19:58 +0800 Subject: [PATCH] fix(context): defer supporting pointer inventory from initial reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../quota-cli-hot-path-compaction-v0.md | 23 ++++++++++ .../quota/progressive_context.ts | 19 +++++++-- .../control_plane/test_context_first_read.py | 42 +++++++++++++++++++ .../progressive_context.test.ts | 20 ++++++++- 4 files changed, 100 insertions(+), 4 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 ae2ec08b9b..a54f2fe66a 100644 --- a/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md +++ b/docs/reference/protocols/quota-cli-hot-path-compaction-v0.md @@ -6,6 +6,29 @@ quota control plane. The full decision is built first. CLI-only projection then retains action authority on the hot path and moves repeated diagnostic detail behind explicit `--include-detail` selectors. +## Captured Initial Navigation + +The TypeScript context owner also bounds `quota context-read --initial-read`. +For the current four-field supporting-address index, the first view retains +the original source identity, unconfirmed coverage, read-before-dependent-work +obligation and complete source instructions. Its pointer array stays unchanged +in the same immutable capture. Read the full list with the original reader +reference and `--select /context_details/unread_context_index/pointers` when +needed. Older, malformed or extended index layouts remain inline in full. +Goal and Task text, acceptance, permissions, stops and settlement are unchanged. +This changes the starting view, not the complete capture or explicit reads. +Include later index reads and their extra calls when comparing total cost; +fewer initial bytes alone do not establish retention, comprehension or benefit. + +TypeScript 上下文 owner 同时约束 `quota context-read --initial-read`。 +对现行四字段补读地址索引,初读保留原始来源身份、覆盖尚未确认的状态、依赖工作前 +必须补读的义务及完整来源指引。地址数组原样保存在同一不可变 capture 中;需要完整 +清单时,沿原 reader reference 读取 +`--select /context_details/unread_context_index/pointers`。 +旧格式、格式不符或增加未知字段的索引仍完整内联。目标、任务正文、验收、权限、 +停止和结算不变;改变的只是起始视图,不是完整 capture 或显式读取。总成本比较须计入 +后续索引补读及额外调用;初读字节减少本身不证明保留、理解或收益。 + ## Ownership Boundary The quota control plane owns decision, precedence, scheduler, interaction, diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index ae47fcde1a..b0ca41608a 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -327,8 +327,21 @@ export function initialContextReadPointers(context: JsonObject, decisionFields: } }); } else include(...planPath); // Unknown plan versions keep their complete obligations. - // Keep the complete unread index reachable after current controls, required - // sources and settlement obligations; its size must not displace them first. - include("context_details", "unread_context_index"); + const details = object(context.context_details), unread = object(details?.unread_context_index); + const source = object(unread?.source); + // The current supporting-address inventory can dwarf the required facts. + // Keep its original source, unknown coverage and read-before-work obligation + // inline; the complete pointer list remains at this same captured address. + // Unknown/future layouts stay whole rather than hiding a new obligation. + if (unread && Object.keys(unread).length === 4 && unread.coverage_confirmed === false + && unread.read_before_dependent_work === true && Array.isArray(unread.pointers) + && unread.pointers.every(pointer => typeof pointer === "string") + && typeof source?.path === "string" && typeof source.sha256 === "string" + && typeof details?.instruction === "string" && details.instruction.length > 0) { + for (const field of ["source", "coverage_confirmed", "read_before_dependent_work"]) + include("context_details", "unread_context_index", field); + include("context_details", "instruction"); + include("context_details", "command_template"); + } else include("context_details", "unread_context_index"); return pointers; } diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index c6f6241041..d70573033f 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -74,6 +74,48 @@ def read_selection(command): return page, selected_context_values(page["content"]) +def test_initial_read_keeps_supporting_index_obligations_and_recovers_its_full_list(tmp_path, decision): + # A complete address inventory is navigation, not the Goal/Task/stop text. + # Exercise the native reader and exact cold path over the same capture. + step = decision["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][0] + step.pop("optional_attachments") + evidence = decision["interaction_contract"]["agent_channel"]["work_context"]["sources"][1]["content"]["graph"] + evidence["writeback_results"] = [{"detail": f"Original evidence {i}"} for i in range(900)] + prefix = "/interaction_contract/agent_channel/work_context/sources/1/content/graph/writeback_results" + pointers = [f"{prefix}/{i}/detail" for i in range(900)] + source = tmp_path / "complete-source.json" + source.write_text(json.dumps(decision), encoding="utf-8") + index = {"coverage_confirmed": False, "read_before_dependent_work": True, + "source": {"path": str(source), "sha256": hashlib.sha256(source.read_bytes()).hexdigest()}, "pointers": pointers} + instruction = "Unread supporting fields remain unconfirmed. Read the applicable source before dependent work." + decision["context_details"] = {"unread_context_index": index, "instruction": instruction} + capture = tmp_path / "capture" + capture.mkdir() + view = native_context_capture_view(decision, capture) + raw = Path(view["recovery"]["path"]).read_bytes() + + page, values = read_selection(view["first_read_command"]) + base = "/context_details/unread_context_index" + assert values[base + "/coverage_confirmed"] is False + assert values[base + "/read_before_dependent_work"] is True + assert values[base + "/source"] == index["source"] + assert values["/context_details/instruction"] == instruction + assert not any(key == base or key.startswith(base + "/pointers") for key in values) + assert values["/goal_boundary"] == decision["goal_boundary"] + assert values["/selected_todo"] == decision["selected_todo"] + goal = "/interaction_contract/agent_channel/work_context/sources/0/content/text" + assert values[goal].endswith("STOP at the original budget.") + assert values["/interaction_contract/cli_channel/settlement_plan/identity"]["turn_instance_id"] == "original" + + detail = read_context_capture(Namespace(capture_file=view["recovery"]["path"], + capture_sha256=view["recovery"]["sha256"], format="json", select=[base + "/pointers"], + offset=0, page_bytes=200000, max_page_output_bytes=200000)) + assert detail["ok"] and detail["eof"] + assert json.loads(detail["content"])["selections"][0]["value"] == pointers + assert Path(view["recovery"]["path"]).read_bytes() == raw + assert json.loads(raw) == decision + + @pytest.mark.parametrize("output_limit", [8192, 64000]) def test_cli_initial_pages_prioritize_required_facts_and_keep_the_complete_index(tmp_path, decision, output_limit): goal = decision["interaction_contract"]["agent_channel"]["work_context"]["sources"][0]["content"] diff --git a/tests/control_plane_ts/progressive_context.test.ts b/tests/control_plane_ts/progressive_context.test.ts index 174fb93d7d..de2e6807e3 100644 --- a/tests/control_plane_ts/progressive_context.test.ts +++ b/tests/control_plane_ts/progressive_context.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import {progressiveContextPlan} from "../../loopx/control_plane/quota/progressive_context.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"; const outcome = {id: "o", objective: "Keep all requirements.", revision: 1, owner_agent_id: "a", todo_ids: ["t"], @@ -11,6 +11,24 @@ const source = {source: "goal_acceptance", reason: "Adopt the current objective. content: {status: "loaded", goal_acceptance_contract: {outcome_requirements: { schema_version: "loopx_goal_outcomes_v0", outcomes: [outcome]}}}}; +test("initial navigation keeps unknown supporting-index obligations whole", () => { + const known = {coverage_confirmed: false, read_before_dependent_work: true, + pointers: ["/supporting/evidence"], source: {path: "complete.json", sha256: "fixture-digest"}}; + const base = "/context_details/unread_context_index"; + for (const index of [{...known, future_obligation: "Preserve the newly required stop."}, + {...known, coverage_confirmed: true}, {...known, read_before_dependent_work: false}, + {...known, pointers: [42]}, {...known, source: null}]) { + 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.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"}}, []), []); +}); + test("only supporting materials move; cumulative obligations and merged source guidance survive", () => { const payload = {ok: true, interaction_contract: {agent_channel: {work_context: {complete: true, sources: [source]}}}}; const before = structuredClone(payload);