From acb9ca11aa34efbe10289f86f8b4cd4b2b446963 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:28:48 +0800 Subject: [PATCH 1/2] fix(context): prioritize current goal and action on full and delta reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 30 ++++--- loopx/cli_commands/quota_capture.py | 5 +- .../control_plane/quota/context_selection.ts | 7 +- .../quota/progressive_context.ts | 11 ++- .../control_plane/test_context_first_read.py | 89 ++++++++++++++++++- 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 75422c4d48..06fcd2e1d2 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -175,10 +175,13 @@ model retention. These separate boundaries prevent transport loss from becoming either a repeated full-input loop or permission to act on missing requirements. No token savings or accepted outcome gains follow from instruction delivery alone. -An enabled native **full** reference for a recognized decision can include -`first_read_command`. It batches current admission/stop facts, the selected task, +An enabled native **full or delta** reference for a recognized decision can include +`first_read_command`, reading its current full capture without an older baseline. +It batches current admission/stop facts, the current action, the selected task, whole Goal/Todo narrative, source availability/revisions and read routes. Detailed history and optional authoring recipes remain expandable from the same capture. +Current narrative sources and their provenance precede other sources, including +large scoped preferences; both retain their complete selected values. The preference source's complete `current` view also stays in the initial read: corrections, retirement/expiry and empty replacements retain their provenance, revision and advisory authority. This captured pre-work view does not replace the @@ -207,8 +210,10 @@ legacy readers still require reconciliation of their unfinished original Turns before retirement; this slice retains their decoder and full-capture recovery. Initial navigation can replace fully selected children with their complete captured parent to avoid repeating addresses. -An unselected child, including optional or unknown material, prevents that merge; -the selected facts and obligations stay identical. Explicit selectors and index +An unselected child, including optional or unknown material, prevents that merge. +An array also stays separate when merging would restore an earlier element order +and undo the owner's reading priority; ancestors cannot erase that boundary. +The selected facts and obligations stay identical. Explicit selectors and index entries keep their original addresses. The option is read-only and exclusive with explicit selectors or discovery; unknown schemas use generic discovery. Pinned explicit references replay their recorded address order after the same @@ -231,8 +236,9 @@ This reorders the same selected facts without reducing the total body or guaranteeing that all requirements fit one page. Earlier initial-selection digests still reject a changed profile; recover from the original full capture. The concrete first read replaces the broad scalar preview and root index when it -fits; unknown schemas or a tight reference budget keep generic discovery. Warm -deltas keep their existing reader, and disabled native delivery is unchanged. +fits; unknown schemas or a tight reference budget keep generic discovery. A warm +delta's existing `command` still reads its wire delta; its initial read and +recovery read current facts directly. Disabled native delivery is unchanged. This is a starting selection, not all required sources: read applicable criteria, evidence and the complete operation/settlement contract before depending on them. The original SHA-bound full recovery remains available; a selected page's EOF @@ -249,14 +255,17 @@ index/detail route. Folded supporting material still has its original read obligations. The generic reader needs no Session Hook for this selection; activation defaults, saved files, Delta and full-read receipts are unchanged. -中文:已启用原生传输的完整引用可给出 `first_read_command`,合并当前准入、停止条件、 -任务和完整 Goal/Todo 正文,以及来源状态、版本与读取入口;历史和可选格式说明仍在 +中文:已启用原生传输的 full 或 delta 引用可给出 `first_read_command`,直接读取本次 +完整捕获,无须先读旧基线。首读合并当前准入、停止条件、动作、任务和完整 Goal/Todo +正文,以及来源状态、版本与读取入口;正文来源及其 provenance 先于其他来源, +包括较大的 scoped preferences,双方完整选中值均保留。历史和可选格式说明仍在 同一捕获中按需展开。它是起始选读,不代表已读全部必要来源;依赖具体验收、证据或 执行/结算契约前仍须完整读取。 偏好来源的完整 `current` 视图也直接呈现:纠偏、退役/过期和空视图替换均保留 原来源、版本及 advisory 权限边界。本次捕获的工作前视图不能替代偏好 owner 要求的 后续偏好相关外部动作前精确作用域新读;偏好及其交付均不授予权限。 -未知 schema、过小引用预算和 warm delta 保留既有路线, +未知 schema 和过小引用预算保留既有路线;warm delta 的 `command` 仍读取原 wire delta, +首读和恢复入口直接读取当前事实。 关闭原生传输的输出不变;完整恢复和原 SHA 校验均保留。只读的 `--initial-read` 不与 显式选读或索引混用。新私有选读引用固定已解析路径和完整选读摘要,续页复用显式 reader,不重新运行首读导航;后续导航改变不能替换原选读。历史仅保存导航配方的引用 @@ -264,7 +273,8 @@ reader,不重新运行首读导航;后续导航改变不能替换原选读 准入或改写旧描述文件。退役这些 legacy reader 前仍须对账原未完成 Turn;本切片 保留历史解码与完整捕获恢复。 首读可把已经完整选中的同级内容合并为其完整父节点,减少地址重复;任何未选中的 -可选或未知内容都会阻止合并,选中事实和义务不变。显式选读和索引仍保留原地址。 +可选或未知内容都会阻止合并;若数组合并会恢复原元素顺序、覆盖 owner 的读取优先级, +该数组及其祖先也不合并。选中事实和义务不变。显式选读和索引仍保留原地址。 固定显式引用在既有规范地址校验后,按记录的地址顺序重放。旧描述符若保存调用方顺序 却绑定规范排序正文的摘要,仅在摘要不匹配后,对同一已验证来源及相同地址集合重试 一次原规范 planner;只返回摘要完全匹配的字节。此兼容路径每个旧分页最多增加一次 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 2c82e99843..880e908e02 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -437,10 +437,11 @@ def _native_context_reference(projected: dict[str, Any], reference: dict[str, An # The reference must fit even under an explicit small caller ceiling. def fits() -> bool: return len(json.dumps(result, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= min(output_bytes, 7900) - if initial_read_pointers and mode == "full": + if initial_read_pointers: overview = result.pop("overview") instruction = result["instruction"] - result["instruction"] = ("Start with first_read_command for current facts and source navigation. " + 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) result["first_read_command"] = context_capture_read_command(current_path, current["sha256"], page_bytes=page_bytes, max_page_output_bytes=output_bytes, initial_read=True) diff --git a/loopx/control_plane/quota/context_selection.ts b/loopx/control_plane/quota/context_selection.ts index 536e569725..205ddce12c 100644 --- a/loopx/control_plane/quota/context_selection.ts +++ b/loopx/control_plane/quota/context_selection.ts @@ -38,8 +38,13 @@ function coalescedInitialPointers(context: JsonObject, pointers: string[]): stri return {complete: false, pointers: []}; const children = Object.entries(value).map(([key, child]) => visit(child, pointer + "/" + key.replaceAll("~", "~0").replaceAll("/", "~1"))); - const complete = children.length > 0 && children.every(child => child.complete); const fields = children.flatMap(child => child.pointers); + // A whole array replays its original element order. Keep prioritized + // elements separate when merging would undo the owner's navigation order; + // propagate that boundary so an ancestor cannot merge them back either. + const preservesOrder = !Array.isArray(value) || fields.every((field, index) => + index === 0 || positions.get(fields[index - 1])! < positions.get(field)!); + const complete = children.length > 0 && children.every(child => child.complete) && preservesOrder; // A single child needs no wrapper; grouping it can increase output bytes. return {complete, pointers: complete && fields.length > 1 ? [pointer] : fields}; }; diff --git a/loopx/control_plane/quota/progressive_context.ts b/loopx/control_plane/quota/progressive_context.ts index 3489be8c17..ae47fcde1a 100644 --- a/loopx/control_plane/quota/progressive_context.ts +++ b/loopx/control_plane/quota/progressive_context.ts @@ -226,13 +226,22 @@ export function initialContextReadPointers(context: JsonObject, decisionFields: include("agent_lane_next_action", "next_step"); include("agent_lane_next_action", "next_action_basis"); include("interaction_contract", "user_channel"); + for (const field of ["primary_action", "delivery_allowed", "must_attempt", "quiet_noop_allowed"]) + include("interaction_contract", "agent_channel", field); include("interaction_contract", "agent_channel", "required_reads"); const work = ["interaction_contract", "agent_channel", "work_context"]; include(...work, "instruction"); include(...work, "user_todos"); const channel = object(interaction.agent_channel); const sources = object(channel?.work_context)?.sources; - if (Array.isArray(sources)) sources.forEach((_source, index) => { + const sourceRows = Array.isArray(sources) ? sources.map((source, index) => ({source, index})) : []; + // A complete scoped preference can be larger than a reader page. Visit + // current narrative sources first, with their availability and provenance, + // before selecting other sources' metadata: complete-parent coalescing keeps + // the first selected child's position. Preserve stable order within each group. + const hasText = (source: unknown): boolean => typeof object(object(source)?.content)?.text === "string"; + sourceRows.sort((left, right) => Number(hasText(right.source)) - Number(hasText(left.source))); + sourceRows.forEach(({source: _source, index}) => { const source = [...work, "sources", index]; for (const field of ["source", "kind", "reason", "ordering", "command"]) include(...source, field); diff --git a/tests/control_plane/test_context_first_read.py b/tests/control_plane/test_context_first_read.py index dd290f4645..c6f6241041 100644 --- a/tests/control_plane/test_context_first_read.py +++ b/tests/control_plane/test_context_first_read.py @@ -11,7 +11,7 @@ import pytest -from loopx.cli_commands.quota_capture import native_context_capture_view, read_context_capture +from loopx.cli_commands.quota_capture import model_context_reference, native_context_capture_view, read_context_capture from loopx.cli_commands.quota_capture import handle_native_context_command from loopx.control_plane.turn_driver import codex_context from loopx.control_plane.turn_driver.decision_capture import capture_page_range @@ -164,6 +164,57 @@ def test_initial_read_coalesces_whole_contract_with_exact_values_and_unknown_obl assert "older-evidence-" not in page["content"] +@pytest.mark.parametrize("complete_sources", [False, True]) +def test_cli_current_goal_and_action_precede_complete_scoped_preferences(tmp_path, decision, complete_sources): + sources = decision["interaction_contract"]["agent_channel"]["work_context"]["sources"] + goal = sources[0]["content"] + goal["text"] = "CURRENT GOAL: compare accepted outcomes; STOP at the original budget." + preferences = {"source": "agent_preferences", "content": { + "status": "loaded", "revision": "preference-current", "source_authority": "advisory_only", + "current": {"items": ["complete scoped preference " * 2000], "future_constraint": False}, + }} + sources.insert(0, preferences) + if complete_sources: + # Even when the entire array is selected, merging it back into its + # original order must not move the large preference ahead of the Goal. + goal.pop("full_criteria") + sources[2:] = [] + channel = decision["interaction_contract"]["agent_channel"] + channel.update(primary_action="Read the current review before writing back.", + delivery_allowed=False, must_attempt=False, quiet_noop_allowed=False) + view = native_context_capture_view(decision, tmp_path) + raw = Path(view["recovery"]["path"]).read_bytes() + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + command = view["first_read_command"] + content, pages = b"", [] + while True: + words = shlex.split(command) + for option in ("--page-bytes", "--max-page-output-bytes"): + words[words.index(option) + 1] = "8192" + result = subprocess.run([sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), + "--runtime-root", str(runtime), "--format", "json", *words[words.index("quota"):]], + cwd=tmp_path, capture_output=True, text=True, check=False) + page = json.loads(result.stdout) + assert result.returncode == 0 and page["ok"] and page["offset"] == len(content), page + assert len(result.stdout.encode()) <= 8192 + assert capture_page_range(page, raw=raw, digest=view["recovery"]["sha256"]) is None + pages.append(page) + content += page["content"].encode() + if page["eof"]: + break + command = page["next_command"] + first = pages[0]["content"] + for value in (goal["text"], goal["status"], goal["revision"], goal["recovery"], channel["primary_action"]): + assert value in first + assert first.index(goal["text"]) < first.index("complete scoped preference") + values = selected_context_values(content) + assert values["/interaction_contract/agent_channel/work_context/sources/0"] == preferences + assert values["/interaction_contract/agent_channel/primary_action"] == channel["primary_action"] + assert values["/interaction_contract/agent_channel/delivery_allowed"] is False + assert json.loads(raw) == decision and Path(view["recovery"]["path"]).read_bytes() == raw + assert not registry.exists() and not runtime.exists() + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_advertised_native_reader_keeps_the_complete_current_contract_across_pages(tmp_path, monkeypatch, provider): from test_quota_settlement_cli import ( @@ -508,7 +559,8 @@ def test_initial_read_is_rejected_before_quota_or_machine_access(tmp_path): assert not (tmp_path / "unopened-registry.json").exists() -def test_warm_delta_keeps_its_existing_reader_and_current_recovery(tmp_path, monkeypatch, decision): +@pytest.mark.parametrize("opaque", [False, True]) +def test_warm_delta_initial_read_uses_current_action_without_the_old_base(tmp_path, monkeypatch, decision, opaque): tmp_path.chmod(0o700) monkeypatch.chdir(tmp_path) monkeypatch.setenv("CODEX_THREAD_ID", "fixture-session") @@ -518,10 +570,41 @@ def test_warm_delta_keeps_its_existing_reader_and_current_recovery(tmp_path, mon directory = tmp_path / name directory.mkdir(mode=0o700) decision["agent_lane_next_action"]["next_action_basis"] = marker + decision["effective_action"] = "normal_run" if marker == "before" else "autonomous_replan_required" + decision["interaction_contract"]["agent_channel"].update( + primary_action=f"Read {marker} action.", delivery_allowed=marker == "before") wire = codex_context.project_codex_context(decision, directory=str(tmp_path), capture_directory=directory, goal_id="fixture", agent_id="agent", surface="quota_packet", capture_id=name) view = native_context_capture_view(wire, directory, context_directory=str(tmp_path)) - assert view["mode"] == "delta" and "first_read_command" not in view + assert view["mode"] == "delta" and "first_read_command" in view current = json.loads(Path(view["recovery"]["path"]).read_bytes()) assert current["agent_lane_next_action"]["next_action_basis"] == "after" assert view["sha256"] != view["recovery"]["sha256"] + baseline = Path(view["baseline_reference"]["path"]) + baseline.rename(baseline.with_suffix(".unavailable")) + navigation = model_context_reference(view) if opaque else view + words = shlex.split(navigation["first_read_command"]) + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + command = [sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), + "--runtime-root", str(runtime), "--format", "json", *words[words.index("quota"):]] + result = subprocess.run(command, cwd=tmp_path, capture_output=True, text=True, check=False) + page = json.loads(result.stdout) + assert result.returncode == 0 and page["ok"] and page["eof"], page + values = selected_context_values(page["content"]) + assert values["/agent_lane_next_action/next_action_basis"] == "after" + assert values["/effective_action"] == "autonomous_replan_required" + assert values["/interaction_contract/agent_channel/primary_action"] == "Read after action." + assert values["/interaction_contract/agent_channel/delivery_allowed"] is False + if opaque: + assert "sha256" not in page + else: + assert page["sha256"] == view["recovery"]["sha256"] + assert "Read before action." not in page["content"] + delta = read_context_capture(Namespace(capture_file=view["path"], capture_sha256=view["sha256"], + format="json", offset=0, page_bytes=64000, max_page_output_bytes=64000)) + assert delta["ok"] and json.loads(delta["content"])["mode"] == "delta" + Path(view["recovery"]["path"]).write_text("{}\n") + failed = subprocess.run(command, cwd=tmp_path, capture_output=True, text=True, check=False) + failure = json.loads(failed.stdout) + assert failed.returncode != 0 and not failure["ok"] and "content" not in failure + assert not registry.exists() and not runtime.exists() From 15bee37524c801e97cc17568852fd35b00f92d21 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:47:34 +0800 Subject: [PATCH 2/2] test(context): preserve priority in array coalescing coverage Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane_ts/context_selection.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/control_plane_ts/context_selection.test.ts b/tests/control_plane_ts/context_selection.test.ts index 013d23c005..43ac3f534b 100644 --- a/tests/control_plane_ts/context_selection.test.ts +++ b/tests/control_plane_ts/context_selection.test.ts @@ -151,7 +151,7 @@ test("initial navigation coalesces complete branches without changing selected f single: {only: "A single address need not grow a container."}, unread: {history: "optional body"}}; // The owner's discovery order differs from object key order. A merged parent // occupies its first selected child's position, preserving that priority. - const pointers = ["/single/only", "/facts/1/evidence", "/facts/0/a~1b", "/facts/0/~0", "/facts/1/allowed"]; + const pointers = ["/single/only", "/facts/0/a~1b", "/facts/0/~0", "/facts/1/evidence", "/facts/1/allowed"]; const before = structuredClone(context); const explicit = select(context, pointers); const initial = capturedContextSelection({context, initial_read: true}, pointers); @@ -169,6 +169,21 @@ test("initial navigation coalesces complete branches without changing selected f assert.deepEqual(context, before); }); +test("initial navigation retains prioritized array elements through complete ancestors", () => { + const context = {complete: {facts: [{text: "Supporting evidence."}, {text: "Current Goal and final stop."}]}}; + const before = structuredClone(context); + const pointers = ["/complete/facts/1/text", "/complete/facts/0/text"]; + const initial = capturedContextSelection({context, initial_read: true}, pointers); + // A complete array or ancestor would replay evidence before the current Goal. + const expected = pointers; + assert.deepEqual((initial.selections as {pointer: string}[]).map(row => row.pointer), expected); + assert.deepEqual(selectedFacts(context, initial.selections as {path: (string | number)[]}[]), + selectedFacts(context, select(context, pointers).selections as {path: (string | number)[]}[])); + const discovery = capturedContextSelection({context, select: [""], list_fields: true}, pointers); + assert.deepEqual(discovery.initial_read_pointers, expected); + assert.deepEqual(context, before); +}); + test("ordinary selectors do not inspect initial-navigation data", () => { const context = {facts: {allowed: false}}; const unused = new Proxy([], {get() { throw new Error("Initial navigation was not requested"); }});