Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions docs/reference/required-work-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,21 @@ marked `required=false` defer their guidance/templates; their routes stay visibl
and the complete captured attachment must be read before use. Unknown plan
versions remain whole. Initial pages do not repeat pointers already present in
their content; the selection digest still prevents a full-input receipt.
Its `context-read --initial-read` uses the same exact-address selector; paged
continuations retain the short option and pin the selection digest. A changed
profile rejects rather than mixing views. Initial navigation can replace fully
Its `context-read --initial-read` uses the same exact-address selector. New private
selection references pin the resolved addresses and complete selection digest;
continuations use the existing explicit reader instead of rerunning initial
navigation. A later navigation change cannot replace the saved selection.
Historical recipe-only references and explicit-file initial cursors still reject
a changed selection digest; recover through the same original full-capture
reference, without another admission or rewriting the saved descriptor. These
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
entries keep their original addresses. The option is read-only and exclusive with explicit
selectors or discovery; unknown schemas use generic discovery.
entries keep their original addresses. The option is read-only and exclusive with
explicit selectors or discovery; unknown schemas use generic discovery.
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.
Expand Down Expand Up @@ -232,7 +239,11 @@ activation defaults, saved files, Delta and full-read receipts are unchanged.
后续偏好相关外部动作前精确作用域新读;偏好及其交付均不授予权限。
未知 schema、过小引用预算和 warm delta 保留既有路线,
关闭原生传输的输出不变;完整恢复和原 SHA 校验均保留。只读的 `--initial-read` 不与
显式选读或索引混用;续页保留短选项和固定的选读摘要,profile 改变则拒绝混读。
显式选读或索引混用。新私有选读引用固定已解析路径和完整选读摘要,续页复用显式
reader,不重新运行首读导航;后续导航改变不能替换原选读。历史仅保存导航配方的引用
及显式文件首读游标在选读摘要变化时仍拒绝,沿同一完整原 capture 引用恢复,不重新
准入或改写旧描述文件。退役这些 legacy reader 前仍须对账原未完成 Turn;本切片
保留历史解码与完整捕获恢复。
首读可把已经完整选中的同级内容合并为其完整父节点,减少地址重复;任何未选中的
可选或未知内容都会阻止合并,选中事实和义务不变。显式选读和索引仍保留原地址。
首读也给出现有验收材料地址和当前结算义务。仅明确 `required=false` 的附件延后完整
Expand Down
5 changes: 4 additions & 1 deletion loopx/cli_commands/quota_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,11 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An
selectors = [item["pointer"] for item in views]
if reference_file is not None and pinned_selection is None:
try:
# Pin the resolved addresses, not the initial navigation
# recipe. Existing explicit readers can replay these exact
# bytes after a navigation change, with both digests checked.
reference_file = write_context_read_reference(path, digest, selection={
"select": None if initial_read else selectors, "initial_read": initial_read,
"select": selectors, "initial_read": False,
"list_fields": list_fields, "list_depth": list_depth, "sha256": expected})
except OSError:
# A verified read must not require a writable capture root.
Expand Down
52 changes: 52 additions & 0 deletions tests/control_plane/test_context_read_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,58 @@ def changed(operation, **kwargs):
assert "content" not in rejected


@pytest.mark.parametrize("legacy", [False, True], ids=["pinned-addresses", "historical-profile"])
def test_initial_reference_navigation_change_preserves_original_read_boundary(tmp_path, monkeypatch, legacy):
payload, machine, model = fixture(tmp_path)
expected = read(model["first_read_command"])
assert expected["ok"] and expected["eof"]
first = read(model["first_read_command"], max_page_output_bytes=3000, page_bytes=3000)
assert first["ok"] and not first["eof"]
reference = Path(first["reference_file"])
if legacy:
# Historical documents stored the navigation recipe, not its addresses.
_, descriptor = read_context_read_reference(str(reference))
reference = write_context_read_reference(Path(machine["path"]), machine["sha256"], selection={
**descriptor["selection"], "select": None, "initial_read": True})
reference_before, capture_before = reference.read_bytes(), Path(machine["path"]).read_bytes()
from loopx.control_plane.quota import turn_envelope

original = turn_envelope._turn_envelope_result

def changed_navigation(operation, **kwargs):
if kwargs.get("initial_read"):
kwargs = {**kwargs, "initial_read": False, "select": ["/criterion"]}
return original(operation, **kwargs)

monkeypatch.setattr(turn_envelope, "_turn_envelope_result", changed_navigation)
# A fresh initial read now has different navigation over the same source.
current = read(model["first_read_command"])
assert json.loads(current["content"])["selections"] == [{"pointer": "/criterion", "value": payload["criterion"]}]
following = read(first["next_command"], reference_file=str(reference))
if legacy:
assert following["ok"] is False and "digest changed" in following["reason"]
assert "content" not in following
recovered = read(model["recovery"]["command"], select=["/goal_boundary", "/selected_todo"])
assert recovered["ok"] and recovered["eof"]
values = {row["pointer"]: row["value"] for row in json.loads(recovered["content"])["selections"]}
else:
parts = [first["content"]]
while True:
assert following["ok"] and following["schema_version"] == "context_capture_selection_page_v1"
assert _context_pages(following) == []
assert following["offset"] == len("".join(parts).encode())
parts.append(following["content"])
if following["eof"]:
break
following = read(following["next_command"])
assert "".join(parts) == expected["content"]
values = {row["pointer"]: row["value"] for row in json.loads("".join(parts))["selections"]}
assert values["/goal_boundary"] == payload["goal_boundary"]
assert values["/selected_todo"] == payload["selected_todo"]
assert reference.read_bytes() == reference_before
assert Path(machine["path"]).read_bytes() == capture_before


@pytest.mark.parametrize("selection", ["fields", "index", "initial"])
def test_selected_read_survives_unwritable_reference_metadata(tmp_path, monkeypatch, selection):
import errno
Expand Down
Loading