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
17 changes: 17 additions & 0 deletions docs/reference/required-work-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,16 @@ not renumber arguments. Inspect the resolved prefix before that token in the
same capture; malformed pointers need syntax repair first. Diagnostics never
echo the pointer or task values, and no part of a rejected batch is delivered.

Reader failures preserve the runtime's `error_kind` and `diagnostic_code` so
programs need not parse `reason`. Selector rejections use `request_rejected`
with `context_selection_invalid_pointer`, `context_selection_invalid_index`
or `context_selection_missing_pointer`. Repair the pointer syntax or inspect
the known parent in the same capture, then retry the corrected batch. Other
runtime failures retain their own classification; startup failures retain
their diagnostic code. These failures do not require restoring a healthy
capture. Missing or damaged capture files still fail closed through the
original file/digest checks. Reading again never reruns admission.

The `context_capture_index_v1` view shares the digest-bound pagination contract.
An expanded index includes its `list_depth`; `next_command` preserves the depth,
selectors and both hashes. Changing depth rejects the prior view's continuation.
Expand Down Expand Up @@ -905,6 +915,13 @@ pointer 从原始摘要绑定的 capture 批量读取详情。提供已有 `--ca
路径前缀;语法错误需先修正 pointer。错误信息不回显路径或任务正文,被拒绝的
批次不会返回部分内容。

读取失败保留 runtime 的 `error_kind` 与 `diagnostic_code`,程序无需解析 `reason`。
选择错误使用 `request_rejected`,原因码分别为 `context_selection_invalid_pointer`、
`context_selection_invalid_index` 或 `context_selection_missing_pointer`。
修正路径语法或在同一 capture 中检查已知父节点,再重试修正后的批次。其他 runtime
故障保留自身分类;启动故障保留原诊断码。这些故障不要求恢复健康的 capture。
文件缺失或损坏仍由原文件/摘要检查拒绝,重新读取不会重新准入。

`context_capture_index_v1` 沿用双摘要分页;展开的目录包含 `list_depth`,
`next_command` 保留深度、路径和两个摘要。更改深度会拒绝旧视图的续读,目录 cursor
也不能用作正文 cursor。遇到输出截断或缺页,用足够的输出预算恢复同一 capture
Expand Down
12 changes: 11 additions & 1 deletion loopx/cli_commands/quota_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,20 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An
except (OSError, ValueError, RecursionError, EffectRuntimeRemoteError, EffectRuntimeStartupError) as exc:
# Recovery is presentation-only: keep failures separate from quota's
# collection error path and retain the existing receipt/capture.
return {"ok": False, "schema_version": "context_capture_page_v1",
result = {"ok": False, "schema_version": "context_capture_page_v1",
"reason": str(exc) if isinstance(exc, (ValueError, EffectRuntimeRemoteError, EffectRuntimeStartupError))
else "original capture could not be read",
"recovery": "Restore the original digest-bound capture; no guard, admission or spend was performed."}
if isinstance(exc, (EffectRuntimeRemoteError, EffectRuntimeStartupError)):
result["diagnostic_code"] = exc.diagnostic_code
result["recovery"] = ("Resolve the reported runtime failure, then retry reading the same digest-bound capture; "
"no guard, admission or spend was performed.")
if isinstance(exc, EffectRuntimeRemoteError):
result["error_kind"] = exc.error_kind
if exc.error_kind == "request_rejected":
result["recovery"] = ("Correct the rejected read request; for unknown fields use --list on a known parent. "
"Keep the original capture and digest; no guard, admission or spend was performed.")
return result


def native_context_capture_view(projected: dict[str, Any], directory: Path, *,
Expand Down
9 changes: 6 additions & 3 deletions loopx/control_plane/quota/context_selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,23 @@ export function capturedContextSelection(request: JsonObject): JsonObject {
const selections = pointers.map(pointer => {
const argument = `--select argument ${argumentsByPointer.get(pointer)}`;
if (pointer !== "" && !pointer.startsWith("/") || /~(?:[^01]|$)/.test(pointer))
throw new EffectRuntimeRequestError(`context selection requires RFC 6901 pointers (${argument}; positions are 1-based)`);
throw new EffectRuntimeRequestError(`context selection requires RFC 6901 pointers (${argument}; positions are 1-based)`,
"context_selection_invalid_pointer");
const tokens = pointer === "" ? [] : pointer.slice(1).split("/").map(token => token.replace(/~1/g, "/").replace(/~0/g, "~"));
const path: (string | number)[] = [];
let value: unknown = context;
for (const token of tokens) {
if (Array.isArray(value)) {
if (!/^(0|[1-9][0-9]*)$/.test(token) || !Number.isSafeInteger(Number(token)) || Number(token) >= value.length)
throw new EffectRuntimeRequestError(`context selection array index is missing or invalid (${argument}, token ${path.length + 1}; positions are 1-based)`);
throw new EffectRuntimeRequestError(`context selection array index is missing or invalid (${argument}, token ${path.length + 1}; positions are 1-based)`,
"context_selection_invalid_index");
path.push(Number(token));
value = value[Number(token)];
} else if (value !== null && typeof value === "object" && Object.hasOwn(value, token)) {
path.push(token);
value = (value as JsonObject)[token];
} else throw new EffectRuntimeRequestError(`context selection pointer does not exist (${argument}, token ${path.length + 1}; positions are 1-based)`);
} else throw new EffectRuntimeRequestError(`context selection pointer does not exist (${argument}, token ${path.length + 1}; positions are 1-based)`,
"context_selection_missing_pointer");
}
// Return the verified address, not a JS-number-coerced copy of the value.
// The file adapter retains original JSON values and owns byte serialization.
Expand Down
90 changes: 90 additions & 0 deletions tests/control_plane/test_context_capture_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from tests.control_plane.codex_checkpoint_fixture import NativeContextReader, last_context_page
from loopx.cli_commands.quota_capture import read_context_capture
from loopx.control_plane.effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError
from loopx.control_plane.turn_driver.decision_capture import capture_page_range
from test_quota_settlement_cli import _run_cli

Expand Down Expand Up @@ -276,3 +277,92 @@ def test_cli_missing_batch_pointer_identifies_original_argument_without_source_l
assert f"--select argument 3, token {token}" in result["reason"]
assert pointer not in result["reason"] and "stop if unavailable" not in json.dumps(result)
assert path.read_bytes() == raw and not registry.exists() and not runtime.exists()


def test_cli_repairs_missing_selection_using_typed_error_and_same_capture(tmp_path):
path, raw, original = fixture_capture(tmp_path)
registry, runtime = tmp_path / "absent.json", tmp_path / "absent-runtime"
command = ["quota", "context-read", "--capture-file", str(path),
"--capture-sha256", hashlib.sha256(raw).hexdigest()]
code, failure = _run_cli(registry, runtime, *command,
"--select", "/large", "--select", "/rows/0/missing", cwd=tmp_path)
assert code != 0 and failure["ok"] is False and "content" not in failure
assert failure["error_kind"] == "request_rejected"
assert failure["diagnostic_code"] == "context_selection_missing_pointer"
assert "--list" in failure["recovery"] and "Restore" not in failure["recovery"]
# Branch on the typed cause, not wording in reason; discover and retry
# against the original capture without another guard or admission.
code, index = _run_cli(registry, runtime, *command, "--select", "/rows/0", "--list", cwd=tmp_path)
assert code == 0 and index["ok"] and index["eof"], index
fields = json.loads(index["content"])["selections"][0]["fields"]
pointer = next(field["pointer"] for field in fields if field["pointer"] == "/rows/0/tail")
code, selected = _run_cli(registry, runtime, *command, "--select", pointer, cwd=tmp_path)
assert code == 0 and selected["ok"] and selected["eof"], selected
assert json.loads(selected["content"])["selections"] == [
{"pointer": pointer, "value": original["rows"][0]["tail"]}]
assert capture_page_range(selected, raw=raw, digest=selected["sha256"]) is None
assert path.read_bytes() == raw and not registry.exists() and not runtime.exists()


@pytest.mark.parametrize("kind,code", [
("request_rejected", "request_too_large"),
("io_transient", "io_busy"),
("io_permanent", "io_permission_denied"),
("internal_failure", "unexpected_handler_error"),
])
def test_reader_preserves_runtime_diagnostics_without_parsing_reason(tmp_path, monkeypatch, kind, code):
from loopx.control_plane.quota import turn_envelope

path, raw, _ = fixture_capture(tmp_path)

def fail(*_args, **_kwargs):
raise EffectRuntimeRemoteError("same opaque reason", error_kind=kind, diagnostic_code=code)

monkeypatch.setattr(turn_envelope, "_turn_envelope_result", fail)
result = read_context_capture(args(path, raw))
assert result["ok"] is False and "content" not in result and "next_command" not in result
assert result["error_kind"] == kind and result["diagnostic_code"] == code
assert result["reason"] == "same opaque reason"
assert "Restore" not in result["recovery"]
assert ("--list" in result["recovery"]) is (kind == "request_rejected")
assert path.read_bytes() == raw


def test_runtime_startup_failure_does_not_request_capture_restoration(tmp_path, monkeypatch):
from loopx.control_plane.quota import turn_envelope

path, raw, _ = fixture_capture(tmp_path)

def fail(*_args, **_kwargs):
raise EffectRuntimeStartupError("runtime unavailable", diagnostic_code="runtime_startup_timeout")

monkeypatch.setattr(turn_envelope, "_turn_envelope_result", fail)
result = read_context_capture(args(path, raw))
assert result["ok"] is False and "content" not in result
assert result["diagnostic_code"] == "runtime_startup_timeout"
assert "runtime" in result["recovery"] and "Restore" not in result["recovery"]
assert path.read_bytes() == raw


@pytest.mark.parametrize("damage", ["missing", "digest", "json"])
def test_capture_failure_cannot_be_repaired_as_a_selector_error(tmp_path, monkeypatch, damage):
from loopx.control_plane.quota import turn_envelope

path, raw, _ = fixture_capture(tmp_path)
request = args(path, raw)
if damage == "missing":
path.unlink()
elif damage == "digest":
path.write_bytes(b"{}")
else:
path.write_bytes(b"{broken")
request.capture_sha256 = hashlib.sha256(path.read_bytes()).hexdigest()

def unexpected(*_args, **_kwargs):
pytest.fail("invalid captures must not reach selection")

monkeypatch.setattr(turn_envelope, "_turn_envelope_result", unexpected)
result = read_context_capture(request)
assert result["ok"] is False and "content" not in result and "next_command" not in result
assert "diagnostic_code" not in result and "error_kind" not in result
assert "Restore the original digest-bound capture" in result["recovery"]
21 changes: 21 additions & 0 deletions tests/control_plane_ts/context_selection.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import {test} from "node:test";
import {evaluateTurnEnvelope} from "../../loopx/control_plane/quota/turn_envelope.ts";
import {effectRuntimeErrorPayload} from "../../loopx/control_plane/effect_runtime_errors.ts";

const select = (context: object, pointers: unknown) => evaluateTurnEnvelope({operation: "context_select", context, select: pointers});

Expand Down Expand Up @@ -70,3 +71,23 @@ test("selection failures identify original argument and token without echoing co
}
assert.throws(() => select(context, ["/z", "secret-".repeat(1000)]), /--select argument 2/);
});

test("selection failures retain machine-readable causes at the runtime boundary", () => {
const context = {rows: [{stop: "private-value"}]};
for (const [pointer, code] of [
["rows", "context_selection_invalid_pointer"],
["/~2", "context_selection_invalid_pointer"],
["/rows/01", "context_selection_invalid_index"],
["/rows/9", "context_selection_invalid_index"],
["/rows/0/missing", "context_selection_missing_pointer"],
["/toString", "context_selection_missing_pointer"],
]) {
assert.throws(() => select(context, ["/rows", pointer]), (error: unknown) => {
const failure = effectRuntimeErrorPayload(error);
assert.equal(failure.kind, "request_rejected");
assert.equal(failure.code, code);
assert.ok(!failure.message.includes(pointer) && !failure.message.includes("private-value"));
return true;
});
}
});
Loading