diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 5759fb9b84..eebd849c70 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -856,6 +856,12 @@ known pointer does not require another index round trip. Batch within the combin output budget, avoid overlapping parent/child selections, and inspect a missing pointer's parent before retrying the rejected batch. These are caller guidelines; selection validation, required reads and authority checks remain unchanged. +When it fits the existing native-reference output ceiling, `field_index` supplies +the complete top-level index of the current recovery capture. Its pointers, +types, byte sizes and member counts match `context-read --list`; it contains no +field bodies and never proves they were read. Select those pointers through +`recovery.command`; a delta's `command` still addresses its patch. Oversized +indexes are omitted whole, leaving `index_command` and full recovery available. Selection errors identify the original `--select` argument and, for a failed lookup, its token position (both 1-based). Sorting and duplicate elimination do not renumber arguments. Inspect the resolved prefix before that token in the @@ -896,6 +902,10 @@ pointer 从原始摘要绑定的 capture 批量读取详情。提供已有 `--ca 发现多层字段。深度默认为 1,可选 1–8;需要更深时选择新的父节点继续。这只限制 单次遍历深度,不限制 capture 容量或可读正文。优先展开相关子树,省略路径才从根开始。 生成的读取指引会提示上述选择,不把查目录变成前置步骤,也不改变工作前的必读要求。 +在原生引用的现有输出上限允许时,`field_index` 直接提供当前 recovery capture 的完整 +顶层目录,其 pointer、类型、字节数和成员数与 `context-read --list` 一致。目录不含 +正文,也不证明已阅读;通过 `recovery.command` 选读这些路径,delta 的 `command` +仍指向补丁。过大目录整体省略,保留 `index_command` 与完整恢复路径。 普通 capture 回复、原生持久配置与压缩后恢复携带同一份选择指引。恢复命令直接指向 `--select`,已知路径无须多查一轮目录。按合计输出预算分批,避免同时选择父节点和 子节点;路径缺失时先查其父节点,再重试被拒绝的批次。这些是调用建议,不改变 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 09261c0f4e..3bea09f99d 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -91,6 +91,24 @@ def fits(result: dict[str, Any]) -> bool: return best +def _context_index_fields(payload: dict[str, Any], children: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Serialize metadata for TS-verified addresses, preserving Python JSON values.""" + fields = [] + for child in children: + value: Any = payload + for key in child["path"]: + value = value[key] + field = {"pointer": child["pointer"], "type": + "object" if isinstance(value, dict) else "array" if isinstance(value, list) + else "null" if value is None else "boolean" if isinstance(value, bool) + else "string" if isinstance(value, str) else "number", + "bytes": len(json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))} + if isinstance(value, (dict, list)): + field["count"] = len(value) + fields.append(field) + return fields + + def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, Any] | None = None, next_page_bytes: int | None = None, next_page_output_bytes: int | None = None) -> dict[str, Any]: @@ -163,20 +181,7 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An for key in item["path"]: selected = selected[key] if list_fields: - fields = [] - for child in item["children"]: - value = payload - for key in child["path"]: - value = value[key] - field = {"pointer": child["pointer"], "type": - "object" if isinstance(value, dict) else "array" if isinstance(value, list) - else "null" if value is None else "boolean" if isinstance(value, bool) - else "string" if isinstance(value, str) else "number", - "bytes": len(json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))} - if isinstance(value, (dict, list)): - field["count"] = len(value) - fields.append(field) - views.append({"pointer": item["pointer"], "fields": fields}) + views.append({"pointer": item["pointer"], "fields": _context_index_fields(payload, item["children"])}) else: views.append({"pointer": item["pointer"], "value": selected}) view_options = {"list_depth": list_depth} if list_depth is not None and list_depth != 1 else {} @@ -273,6 +278,24 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, # The reference must fit even under an explicit small caller ceiling. def fits(): return len(json.dumps(result, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= min(output_bytes, 7900) + # The same complete top-level index as `context-read --list`, bound to + # recovery (current facts), not a delta's patch keys. This saves an index + # tool roundtrip when it fits; it neither supplies source contents nor + # acknowledges their delivery. Deeper navigation keeps its existing route. + from ..control_plane.quota.turn_envelope import _turn_envelope_result + + # Depth-one addressing needs keys only, not another transport of every + # source body. Metadata bytes/types are read from the original JSON below. + try: + plan = _turn_envelope_result("context_select", context=dict.fromkeys(current_document), select=[""], list_fields=True) + except (EffectRuntimeRemoteError, EffectRuntimeStartupError): + # An optional index cannot turn a valid immutable file reference into + # a failed delivery. Ordinary full recovery remains a file-only read. + pass + else: + result["field_index"] = _context_index_fields(current_document, plan["selections"][0]["children"]) + if not fits(): + result.pop("field_index") # Optional metadata must never displace recovery or instructions. if not fits(): result.pop("overview") if not fits(): diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 7b16521175..dc1f3e4ea4 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -321,6 +321,8 @@ def test_native_reference_exposes_file_continuity_and_preserves_full_recovery(tm assert view["continuity_basis"] == "captured_file" assert view["baseline_reference"]["path"] == first["recovery"]["path"] assert view["recovery"]["path"] == second["recovery"]["path"] + assert [field["pointer"] for field in view["field_index"]] == list("/" + key for key in payload) + assert "/changes" not in [field["pointer"] for field in view["field_index"]], "index addresses current recovery, never the delta wire" for instruction in (view["instruction"], second["recovery"]["instruction"]): assert "Native input observation independently verifies" not in instruction deliver(tmp_path, event, capture_pages(view["path"], view["sha256"])) diff --git a/tests/control_plane/test_context_capture_selection.py b/tests/control_plane/test_context_capture_selection.py index 1e17306737..ffd23052a8 100644 --- a/tests/control_plane/test_context_capture_selection.py +++ b/tests/control_plane/test_context_capture_selection.py @@ -3,6 +3,7 @@ import hashlib import json import shlex +from pathlib import Path import pytest @@ -50,6 +51,56 @@ def args(path, raw): offset=0, page_bytes=256, max_page_output_bytes=2000, format="json", select=["/rows/0"])) +def test_native_reference_reuses_the_exact_index_before_selecting_details(tmp_path): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import native_context_capture_view + + original = {"requirements": {"text": "Preserve acceptance and stop conditions. " * 100}, + "a/b~": [None, True, 9007199254740993], "nullable": None} + reference = native_context_capture_view(original, tmp_path) + parser = build_parser() + indexed = read_context_capture(parser.parse_args(shlex.split(reference["index_command"])[1:])) + assert indexed["ok"] and indexed["eof"] + assert reference["field_index"] == json.loads(indexed["content"])["selections"][0]["fields"] + assert [item["pointer"] for item in reference["field_index"]] == ["/requirements", "/a~1b~0", "/nullable"] + assert "Preserve acceptance" not in json.dumps(reference) + assert "content" not in reference and "eof" not in reference + assert capture_page_range(reference, raw=b"", digest=reference["sha256"]) is None + # The inline index replaces only discovery, never the content read. + selected = read_context_capture(parser.parse_args(shlex.split(reference["recovery"]["command"])[1:] + + ["--select", "/a~1b~0"])) + assert selected["ok"] + assert json.loads(selected["content"])["selections"][0]["value"] == original["a/b~"] + + +def test_large_inline_index_is_omitted_whole_without_displacing_navigation(tmp_path): + from loopx.cli_commands.quota_capture import native_context_capture_view + + original = {"field-" + str(index): {"preserve": index} for index in range(300)} + reference = native_context_capture_view(original, tmp_path) + assert "field_index" not in reference + assert "recovery" in reference and "index_command" in reference and "select_command" in reference + assert len(json.dumps(reference, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 7900 + assert json.loads(Path(reference["recovery"]["path"]).read_text()) == original + + +def test_unavailable_optional_index_keeps_file_only_recovery(tmp_path, monkeypatch): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import native_context_capture_view + from loopx.control_plane.effect_runtime import EffectRuntimeStartupError + from loopx.control_plane.quota import turn_envelope + + def unavailable(*args, **kwargs): + raise EffectRuntimeStartupError("Synthetic typed runtime unavailable", diagnostic_code="fixture_unavailable") + + monkeypatch.setattr(turn_envelope, "_turn_envelope_result", unavailable) + original = {"requirements": "Keep every requirement and stop condition."} + reference = native_context_capture_view(original, tmp_path) + assert reference["ok"] and "field_index" not in reference + page = read_context_capture(build_parser().parse_args(shlex.split(reference["recovery"]["command"])[1:])) + assert page["ok"] and json.loads(page["content"]) == original + + def test_native_reference_reader_uses_real_pages_before_recording_delivery(tmp_path): path, raw, _ = fixture_capture(tmp_path) options = args(path, raw)