From bdf1477ee6d3625c7bc8af75fdfe2f2956bfa2be Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:26:28 +0800 Subject: [PATCH 1/6] test(release): align bounded native readers and typed wait assertions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/product/releases/v2.2.0.md | 6 ++++-- tests/control_plane/test_codex_context_capture.py | 9 ++++++--- tests/control_plane/test_dependency_vision_wait_cli.py | 2 +- .../control_plane/test_machine_guard_context_contract.py | 1 - 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/product/releases/v2.2.0.md b/docs/product/releases/v2.2.0.md index 7546c08474..45b3168013 100644 --- a/docs/product/releases/v2.2.0.md +++ b/docs/product/releases/v2.2.0.md @@ -74,7 +74,8 @@ Native differential context requires qualified host lifecycle delivery, not mere - [@JasonBuildAI](https://github.com/JasonBuildAI) — completed-Turn stop races, interrupted canonical creation, signed Windows updates, DSH launch guidance and Unicode/privacy repairs. [#7477](https://github.com/loopx-project/loopx/pull/7477), [#7381](https://github.com/loopx-project/loopx/pull/7381), [#6854](https://github.com/loopx-project/loopx/pull/6854), [#7473](https://github.com/loopx-project/loopx/pull/7473), [#7247](https://github.com/loopx-project/loopx/pull/7247), [#7251](https://github.com/loopx-project/loopx/pull/7251), [#7439](https://github.com/loopx-project/loopx/pull/7439). - [@Jiaqiiii523](https://github.com/Jiaqiiii523) — attached-conversation regressions and Lark upload acknowledgement. [#7293](https://github.com/loopx-project/loopx/pull/7293), [#7395](https://github.com/loopx-project/loopx/pull/7395). - [@Jim-jimu](https://github.com/Jim-jimu) — Pi input-driven pause and Explore input/zero-evidence semantics. [#7401](https://github.com/loopx-project/loopx/pull/7401), [#7303](https://github.com/loopx-project/loopx/pull/7303), [#7143](https://github.com/loopx-project/loopx/pull/7143), [#7239](https://github.com/loopx-project/loopx/pull/7239). -- [@hhyykk](https://github.com/hhyykk) — shared TypeScript decision ownership, complete canonical reads, recovery, queued-session ownership and explicit source-content pins. [#7462](https://github.com/loopx-project/loopx/pull/7462), [#7325](https://github.com/loopx-project/loopx/pull/7325), [#7298](https://github.com/loopx-project/loopx/pull/7298), [#6874](https://github.com/loopx-project/loopx/pull/6874), [#6855](https://github.com/loopx-project/loopx/pull/6855), [#7321](https://github.com/loopx-project/loopx/pull/7321), [#7386](https://github.com/loopx-project/loopx/pull/7386), [#7136](https://github.com/loopx-project/loopx/pull/7136), [#7137](https://github.com/loopx-project/loopx/pull/7137), [#7131](https://github.com/loopx-project/loopx/pull/7131). +- [@LIHUA919](https://github.com/LIHUA919) — packaged-browser fixture startup, rendering/focus and research-reading regressions. [#7508](https://github.com/loopx-project/loopx/pull/7508). +- [@hhyykk](https://github.com/hhyykk) — shared TypeScript decision ownership, complete canonical reads, recovery, queued-session ownership and explicit source-content pins. [#7462](https://github.com/loopx-project/loopx/pull/7462), [#7325](https://github.com/loopx-project/loopx/pull/7325), [#7411](https://github.com/loopx-project/loopx/pull/7411), [#7298](https://github.com/loopx-project/loopx/pull/7298), [#6874](https://github.com/loopx-project/loopx/pull/6874), [#6855](https://github.com/loopx-project/loopx/pull/6855), [#7321](https://github.com/loopx-project/loopx/pull/7321), [#7386](https://github.com/loopx-project/loopx/pull/7386), [#7136](https://github.com/loopx-project/loopx/pull/7136), [#7137](https://github.com/loopx-project/loopx/pull/7137), [#7131](https://github.com/loopx-project/loopx/pull/7131). - [@mikamikasuki](https://github.com/mikamikasuki) — Lark timestamp precision and encoded local-path redaction. [#6084](https://github.com/loopx-project/loopx/pull/6084), [#5978](https://github.com/loopx-project/loopx/pull/5978). - [@songoow](https://github.com/songoow) — mobile task modality, managed observer ownership, report routing and offline analysis cancellation. [#7410](https://github.com/loopx-project/loopx/pull/7410), [#7354](https://github.com/loopx-project/loopx/pull/7354), [#7353](https://github.com/loopx-project/loopx/pull/7353), [#7352](https://github.com/loopx-project/loopx/pull/7352), [#7415](https://github.com/loopx-project/loopx/pull/7415). - [@steven-kid](https://github.com/steven-kid) — original evidence search and revision comparison, retained-work search and report downloads. [#7370](https://github.com/loopx-project/loopx/pull/7370), [#7129](https://github.com/loopx-project/loopx/pull/7129), [#7228](https://github.com/loopx-project/loopx/pull/7228), [#7041](https://github.com/loopx-project/loopx/pull/7041), [#7048](https://github.com/loopx-project/loopx/pull/7048), [#7297](https://github.com/loopx-project/loopx/pull/7297). @@ -347,7 +348,8 @@ Python 3.11+、Node 22.22.3+(推荐 24 LTS)。沿原安装 owner 更新; - [@JasonBuildAI](https://github.com/JasonBuildAI) — 已完成 Turn 的停止竞态、canonical 创建恢复、Windows 签名更新、DSH 启动说明及 Unicode/隐私修复. [#7477](https://github.com/loopx-project/loopx/pull/7477), [#7381](https://github.com/loopx-project/loopx/pull/7381), [#6854](https://github.com/loopx-project/loopx/pull/6854), [#7473](https://github.com/loopx-project/loopx/pull/7473), [#7247](https://github.com/loopx-project/loopx/pull/7247), [#7251](https://github.com/loopx-project/loopx/pull/7251), [#7439](https://github.com/loopx-project/loopx/pull/7439). - [@Jiaqiiii523](https://github.com/Jiaqiiii523) — 原生绑定会话回归与飞书上传确认. [#7293](https://github.com/loopx-project/loopx/pull/7293), [#7395](https://github.com/loopx-project/loopx/pull/7395). - [@Jim-jimu](https://github.com/Jim-jimu) — Pi 输入暂停,以及 Explore 输入与零证据语义. [#7401](https://github.com/loopx-project/loopx/pull/7401), [#7303](https://github.com/loopx-project/loopx/pull/7303), [#7143](https://github.com/loopx-project/loopx/pull/7143), [#7239](https://github.com/loopx-project/loopx/pull/7239). -- [@hhyykk](https://github.com/hhyykk) — 共享 TypeScript 决策归属、完整 canonical 读取、恢复、排队会话归属与显式来源内容固定. [#7462](https://github.com/loopx-project/loopx/pull/7462), [#7325](https://github.com/loopx-project/loopx/pull/7325), [#7298](https://github.com/loopx-project/loopx/pull/7298), [#6874](https://github.com/loopx-project/loopx/pull/6874), [#6855](https://github.com/loopx-project/loopx/pull/6855), [#7321](https://github.com/loopx-project/loopx/pull/7321), [#7386](https://github.com/loopx-project/loopx/pull/7386), [#7136](https://github.com/loopx-project/loopx/pull/7136), [#7137](https://github.com/loopx-project/loopx/pull/7137), [#7131](https://github.com/loopx-project/loopx/pull/7131). +- [@LIHUA919](https://github.com/LIHUA919) — 打包浏览器 fixture 启动、渲染/焦点与研究阅读回归. [#7508](https://github.com/loopx-project/loopx/pull/7508). +- [@hhyykk](https://github.com/hhyykk) — 共享 TypeScript 决策归属、完整 canonical 读取、恢复、排队会话归属与显式来源内容固定. [#7462](https://github.com/loopx-project/loopx/pull/7462), [#7325](https://github.com/loopx-project/loopx/pull/7325), [#7411](https://github.com/loopx-project/loopx/pull/7411), [#7298](https://github.com/loopx-project/loopx/pull/7298), [#6874](https://github.com/loopx-project/loopx/pull/6874), [#6855](https://github.com/loopx-project/loopx/pull/6855), [#7321](https://github.com/loopx-project/loopx/pull/7321), [#7386](https://github.com/loopx-project/loopx/pull/7386), [#7136](https://github.com/loopx-project/loopx/pull/7136), [#7137](https://github.com/loopx-project/loopx/pull/7137), [#7131](https://github.com/loopx-project/loopx/pull/7131). - [@mikamikasuki](https://github.com/mikamikasuki) — 飞书时间戳精度与编码本地路径脱敏. [#6084](https://github.com/loopx-project/loopx/pull/6084), [#5978](https://github.com/loopx-project/loopx/pull/5978). - [@songoow](https://github.com/songoow) — 移动任务交互、受管观察者归属、报告路由与离线分析取消. [#7410](https://github.com/loopx-project/loopx/pull/7410), [#7354](https://github.com/loopx-project/loopx/pull/7354), [#7353](https://github.com/loopx-project/loopx/pull/7353), [#7352](https://github.com/loopx-project/loopx/pull/7352), [#7415](https://github.com/loopx-project/loopx/pull/7415). - [@steven-kid](https://github.com/steven-kid) — 原始证据搜索与版本比较、历史工作搜索及报告下载. [#7370](https://github.com/loopx-project/loopx/pull/7370), [#7129](https://github.com/loopx-project/loopx/pull/7129), [#7228](https://github.com/loopx-project/loopx/pull/7228), [#7041](https://github.com/loopx-project/loopx/pull/7041), [#7048](https://github.com/loopx-project/loopx/pull/7048), [#7297](https://github.com/loopx-project/loopx/pull/7297). diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 32178814ad..612587808d 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -760,11 +760,14 @@ def test_native_code_mode_pragma_retains_delta_and_recovers(tmp_path, monkeypatc @pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) def test_native_known_history_budget_keeps_larger_batches_and_recovery(tmp_path, monkeypatch, surface): # The unknown loopback model otherwise inherits a byte-based fallback. - # Match a declared 10k-token history budget only in this separate case; - # existing low-budget/truncation coverage and host defaults stay intact. + # This lower-than-generated 10k-token history limit applies to the whole + # eight-page batch. Bound each serialized public read to 4k bytes, leaving + # framing headroom under its 40k-byte fallback. The full capture, eight-read + # batching, recovery and independent byte/cursor assertions remain intact. test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, "code_mode", surface, False, True, True, "missing", shared_root=True, - native_pragma=True, native_read_batch=8, native_history_tokens=10000) + native_pragma=True, native_read_batch=8, native_history_tokens=10000, + reader_output_bytes=4000) @pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) diff --git a/tests/control_plane/test_dependency_vision_wait_cli.py b/tests/control_plane/test_dependency_vision_wait_cli.py index 4ff33abc0c..da56e35e90 100644 --- a/tests/control_plane/test_dependency_vision_wait_cli.py +++ b/tests/control_plane/test_dependency_vision_wait_cli.py @@ -277,7 +277,7 @@ def test_unknown_dependency_keeps_bounded_supervision(public_goal): _, waiting = wait_on(goal, "todo_done", missing_source=True) packet = guard(goal) assert packet["vision_wait_state"]["selected_todo_id"] == waiting - assert "target_status" not in packet["vision_wait_state"]["resume_condition"] + assert packet["vision_wait_state"]["resume_condition"].get("target_status") is None assert packet["interaction_contract"]["agent_channel"]["must_attempt"] is True assert set(packet["scheduler_hint"]["unchanged_poll"]["limits"].values()) == {3} diff --git a/tests/control_plane/test_machine_guard_context_contract.py b/tests/control_plane/test_machine_guard_context_contract.py index fed83ec67d..c700de872a 100644 --- a/tests/control_plane/test_machine_guard_context_contract.py +++ b/tests/control_plane/test_machine_guard_context_contract.py @@ -150,4 +150,3 @@ def run(argv, **kwargs): result = json.loads(control.run_cli(args, legacy_args=legacy)) assert result == {"ok": True, "should_run": False} assert len(calls) == 2 and os.environ["LOOPX_CONTEXT_DELIVERY"] == "on" - From f44c7ae84a010fb3fa26735bd69a1c9e90889dab Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:42:38 +0800 Subject: [PATCH 2/6] test(context): bound every read in the native batch Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 612587808d..466b3ed924 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -430,13 +430,16 @@ def response(request, number, message): return tool(cmd, number) def tool(cmd, number): - if reader_output_bytes and "context-read" in shlex.split(cmd): - # Exercise the existing public reader with a budget qualified by - # this real host fixture, retaining the original capture and cursor. - argv = shlex.split(cmd) + def bounded_read(command): + if not reader_output_bytes or "context-read" not in shlex.split(command): + return command + # Apply the selected public reader budget to every batched read, + # retaining the original capture, digest and cursor. + argv = shlex.split(command) argv[argv.index("--page-bytes") + 1] = str(reader_output_bytes) argv[argv.index("--max-page-output-bytes") + 1] = str(reader_output_bytes) - cmd = shlex.join(argv) + return shlex.join(argv) + cmd = bounded_read(cmd) if host == "code_mode": pragma = ('// @exec: {"max_output_tokens":24000}\n' if automatic_output_budget else '// @exec: {"max_output_tokens":12000}\n' if native_pragma else "") @@ -445,7 +448,7 @@ def tool(cmd, number): # unchanged native turn deadline before same-capture recovery ends. commands = [cmd] while stages["pages"] and len(commands) < native_read_batch: - commands.append(stages["pages"].pop(0)) + commands.append(bounded_read(stages["pages"].pop(0))) if adaptive: code = "".join("{const r=await tools.exec_command(" + json.dumps({ "cmd": command, "login": False, "max_output_tokens": 22000 if automatic_output_budget and "context-read" in shlex.split(command) else 11000 if reader_output_bytes else 2000 if bounded_pages else 2400}) From 8f73acf7b04b5f0e770832a0a1522c358d7c2125 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:44:16 +0800 Subject: [PATCH 3/6] test(context): align batch offsets with bounded wire pages Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 466b3ed924..c18504245b 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -236,6 +236,10 @@ def pages(name, path): # Plan only read offsets. Every actual model-facing page must # come from the public CLI, including digest verification. page_size = 3072 if adaptive or host == "code_mode" else 5000 + if reader_output_bytes and not bounded_pages: + # Offset planning must match the bounded payload, with room for + # the serialized page metadata and escaped source text. + page_size = min(page_size, reader_output_bytes // 2) offsets, start, count = [0], 0, 0 for character in raw.decode(): size = len(character.encode()) @@ -436,8 +440,13 @@ def bounded_read(command): # Apply the selected public reader budget to every batched read, # retaining the original capture, digest and cursor. argv = shlex.split(command) - argv[argv.index("--page-bytes") + 1] = str(reader_output_bytes) - argv[argv.index("--max-page-output-bytes") + 1] = str(reader_output_bytes) + requested = argv.index("--page-bytes") + 1 + argv[requested] = str(reader_output_bytes if bounded_pages else + min(int(argv[requested]), reader_output_bytes // 2)) + if "--max-page-output-bytes" in argv: + argv[argv.index("--max-page-output-bytes") + 1] = str(reader_output_bytes) + else: + argv.extend(["--max-page-output-bytes", str(reader_output_bytes)]) return shlex.join(argv) cmd = bounded_read(cmd) if host == "code_mode": From 76dc4ec07d2b0902e67b14e3bb461260696a2681 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:23:16 +0800 Subject: [PATCH 4/6] fix(context): bound initial native frames with the explicit output limit Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/product/releases/v2.2.0.md | 4 +- docs/reference/required-work-context.md | 16 +++-- loopx/cli_commands/quota.py | 3 +- loopx/cli_commands/quota_capture.py | 35 ++++++++-- loopx/cli_commands/quota_registration.py | 2 +- .../test_codex_context_capture.py | 29 ++++++--- .../test_context_delivery_cli.py | 65 ++++++++++++++----- 7 files changed, 118 insertions(+), 36 deletions(-) diff --git a/docs/product/releases/v2.2.0.md b/docs/product/releases/v2.2.0.md index 45b3168013..c689e9fef9 100644 --- a/docs/product/releases/v2.2.0.md +++ b/docs/product/releases/v2.2.0.md @@ -106,7 +106,7 @@ loopx benchmark trajectory-assessment-cancel --directory "$ANALYSIS_DIR" --forma ``` ### Codex differential context -**Activation:** Default off. In Settings → This machine → Capabilities → Differential context → Codex, switch on through the existing exact-revision transaction. Supported native Codex App heartbeat, SSH Goal and explicit benchmark heartbeat consumers also require their exact native hook/configuration setup for the next supported session. A saved preference alone does not initialize a loaded thread. +**Activation:** Default off. In Settings → This machine → Capabilities → Differential context → Codex, switch on through the existing exact-revision transaction. Supported native Codex App heartbeat, SSH Goal and explicit benchmark heartbeat consumers also require their exact native hook/configuration setup for the next supported session. A saved preference alone does not initialize a loaded thread. For a smaller tool-output budget, add --max-page-output-bytes 7900 to the generated guard or context-project command; follow its bounded continuation and recovery commands, and keep aggregate batch output within the host limit. **Validation:** Read back the machine setting, render context-config for the actual Goal/Agent, then verify native lifecycle delivery and complete digest-bound pages. Setup and hooks/list discovery alone do not prove actual delta use or model retention. @@ -378,7 +378,7 @@ loopx benchmark trajectory-assessment-cancel --directory "$ANALYSIS_DIR" --forma ``` #### Codex differential context -**启用:** 默认关闭。在「设置 → 本机 → 能力 → 差分上下文 → Codex」开启,沿原精确 revision 事务保存。受支持的 Codex App heartbeat、SSH Goal 与显式 benchmark heartbeat,还须为下一次受支持会话完成准确 hooks/配置。保存偏好不初始化已载入线程。 +**启用:** 默认关闭。在「设置 → 本机 → 能力 → 差分上下文 → Codex」开启,沿原精确 revision 事务保存。受支持的 Codex App heartbeat、SSH Goal 与显式 benchmark heartbeat,还须为下一次受支持会话完成准确 hooks/配置。保存偏好不初始化已载入线程。宿主输出预算较小时,可在已有 guard 或 context-project 命令追加 --max-page-output-bytes 7900;沿有界续页与原恢复命令读取,批量输出仍须满足宿主总预算。 **验证:** 读回机器设置,针对实际 Goal/Agent 生成 context-config,并验证原生生命周期和完整 digest 绑定分页。设置成功或 hooks/list 可发现不等于实际差分使用、模型保留。 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 8aada9fa79..f522708571 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -287,8 +287,12 @@ recovery without another admission. Inline and continuation pages use the same 64000-byte output budget, including recovery, instructions, escaping, commands and newline. Content shrinks at UTF-8 boundaries to fit that complete stdout; captures that fit finish inline. This default does not depend on a shell output -declaration. Explicit smaller reader budgets remain available to recover a -truncated range of the same capture. Consume the inline +declaration. An explicit `--max-page-output-bytes` also bounds the initial enabled +`should-run` or `context-project` frame, its continuations and full-recovery +command. For example, append `--max-page-output-bytes 7900` to the existing guard +command for a smaller tool-output budget. This does not change host history +limits; a batch must fit their aggregate budget too. Explicit smaller reader +budgets remain available to recover a truncated range of the same capture. Consume the inline page before following next_command; when the retained-history limit is unknown, read one bounded output at a time. This is not a guarantee of model retention. Forward exec stdout directly in code mode. Repeating the packet in @@ -626,7 +630,8 @@ arguments also defaults to 64000 content bytes under a 64000-byte output cap. Markdown retains its 4096-byte content default. Explicit page/output arguments retain their prior semantics; a page above 8192 bytes still requires a sufficient output cap, within the existing capture limit. The JSON-only -`--max-page-output-bytes` shrinks content at UTF-8 boundaries and persists in +`--max-page-output-bytes` also bounds the initial enabled native guard/projection +frame; it shrinks content at UTF-8 boundaries and persists in `next_command`, including for explicitly smaller same-capture recovery. An insufficient cap returns an error without a zero-progress page; increase it for the same capture. Error messages are not subject to an impossible page limit. @@ -659,7 +664,10 @@ input retention. 请求 64000 字节正文;Markdown 保留 4096 字节正文默认值。显式分页/输出参数保留 原有语义:超过 8192 字节的页仍须足够的输出上限,且不超过既有 capture 限制。 JSON 专用参数 `--max-page-output-bytes` 按 UTF-8 边界缩减正文,并在 `next_command` -中保留,显式更小的同捕获恢复也一样。上限不足时返回错误,不返回无进展分页; +中保留,也约束开启原生投递的 `should-run`/`context-project` 首次输出及完整恢复命令。 +例如在已有 guard 命令后追加 `--max-page-output-bytes 7900`;省略时仍为 64000 字节。 +该上限不修改宿主历史容量,批量输出还须满足总预算。显式更小的同捕获恢复也一样。 +上限不足时返回错误,不返回无进展分页; 增大上限后重读同一捕获。错误消息不受无法满足的分页上限限制。 读取器不修改宿主或保留历史设置。历史 Codex 0.162.1 定向验证采用 10,000 token 历史 设置和 12,000 token code-mode 输出预算,每次直接转发一份 stdout;该观察不证明 diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index d63e3d3a34..c6c5b7f319 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -451,7 +451,8 @@ def _emit_quota_result( "command": context_capture_read_command(original, capture_id), } payload = native_context_capture_view(payload, capture_directory, context_directory=directory, - metadata={"capture_sha256": capture_id, "recovery": recovery}) + metadata={"capture_sha256": capture_id, "recovery": recovery}, + max_page_output_bytes=getattr(args, "max_page_output_bytes", None)) except (QuotaDecisionTransportError, OSError, ValueError) as exc: if not isinstance(exc, QuotaDecisionTransportError): exc = QuotaDecisionTransportError( diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index fdecb5d581..7e71080ed0 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -141,7 +141,8 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An def native_context_capture_view(projected: dict[str, Any], directory: Path, *, context_directory: str | None = None, - metadata: dict[str, Any] | None = None) -> dict[str, Any]: + metadata: dict[str, Any] | None = None, + max_page_output_bytes: int | None = None) -> dict[str, Any]: """One paged presentation for ordinary and already-captured native calls. The guard/projector already ran. Only serialize its wire and prefetch the @@ -149,6 +150,8 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, """ from ..control_plane.turn_driver.decision_capture import write_decision_capture + if max_page_output_bytes is not None and max_page_output_bytes <= 0: + raise QuotaDecisionTransportError("Native context output limit must be positive.") wire = directory / "delivery.json" write_decision_capture(wire, projected) reference = {"path": str(wire), "sha256": hashlib.sha256(wire.read_bytes()).hexdigest()} @@ -158,10 +161,18 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, if mode == "full": reference = {key: projected["recovery"][key] for key in ("path", "sha256")} page_bytes, output_bytes = native_context_page_budget() + if max_page_output_bytes is not None: + output_bytes = min(output_bytes, max_page_output_bytes) + page_bytes = min(page_bytes, output_bytes) reference["command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], page_bytes=page_bytes, max_page_output_bytes=output_bytes) result = {"ok": True, **(metadata or {}), "mode": mode, **reference, **({"target": target} if target else {})} + if max_page_output_bytes is not None and isinstance(result.get("recovery"), dict): + recovery = result["recovery"] + result["recovery"] = {**recovery, "command": context_capture_read_command( + Path(recovery["path"]), recovery["sha256"], page_bytes=page_bytes, + max_page_output_bytes=output_bytes)} # The codec puts native continuity semantics before its change tree. Keep # this shared transport frame neutral instead of copying that instruction # or replacing an assumption with a claim of confirmed final input. @@ -254,7 +265,8 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: native_prepared = True native_view = native_context_capture_view(projected, cache, context_directory=directory, metadata={"recovery": full_reference, "capture_sha256": args.capture_sha256, - "context_delivery": activation}) + "context_delivery": activation}, + max_page_output_bytes=getattr(args, "max_page_output_bytes", None)) reference = {key: native_view[key] for key in ("path", "sha256", "command")} mode, target = native_view["mode"], native_view.get("target") except (QuotaDecisionTransportError, OSError, ValueError, TypeError, RecursionError) as exc: @@ -455,8 +467,13 @@ def handoff_decision(directory: Path | None, socket_path: str | None, def handle_native_context_command(args: argparse.Namespace, *, print_payload: Callable[..., None], registry_path: Path | None = None, runtime_root_arg: str | None = None) -> int | None: """Dispatch native presentation/lifecycle IO before any quota authority read.""" - if getattr(args, "max_page_output_bytes", None) is not None and args.quota_command != "context-read": - print_payload({"ok": False, "reason": "max-page-output-bytes is only supported by JSON context-read"}, + has_output_limit = getattr(args, "max_page_output_bytes", None) is not None + budget_scope_error = ("max-page-output-bytes is only supported by JSON context-read " + "or enabled native should-run/context-project") + if has_output_limit and args.quota_command != "context-read" and ( + args.quota_command not in ("should-run", "context-project") or args.format != "json" + or getattr(args, "context_delivery", None) == "off"): + print_payload({"ok": False, "reason": budget_scope_error}, args.format, lambda value: str(value["reason"])) return 1 if registry_path is not None: @@ -468,6 +485,16 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca print_payload({"ok": False, "reason": str(exc)}, args.format, lambda value: json.dumps(value, ensure_ascii=False, indent=2)) return 1 + if has_output_limit and args.quota_command != "context-read": + try: + if not native_context_activation(args)["enabled"]: + raise QuotaCommandValidationError(budget_scope_error) + if args.max_page_output_bytes <= 0: + raise QuotaCommandValidationError("max-page-output-bytes must be positive") + except QuotaCommandValidationError as exc: + print_payload({"ok": False, "reason": str(exc)}, args.format, + lambda value: str(value["reason"])) + return 1 if args.quota_command in ("context-config", "context-hook"): try: activation = native_context_activation(args) diff --git a/loopx/cli_commands/quota_registration.py b/loopx/cli_commands/quota_registration.py index 3ee2c48513..7789a3f800 100644 --- a/loopx/cli_commands/quota_registration.py +++ b/loopx/cli_commands/quota_registration.py @@ -231,7 +231,7 @@ def register_quota_command( "Larger pages require a sufficient --max-page-output-bytes within the capture limit; " "use only with known sufficient direct and retained-history output budgets.") quota_parser.add_argument("--max-page-output-bytes", type=int, - help="For JSON context-read: optional positive byte limit for successful page stdout, including JSON framing and newline. " + help="For JSON context-read and enabled native should-run/context-project: optional positive byte limit for successful page stdout, including JSON framing and newline. " "Content shrinks at UTF-8 boundaries; next_command preserves the limit. An insufficient limit returns an error.") quota_parser.add_argument( "--turn-instance-id", diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index c18504245b..1bfee07050 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -131,7 +131,11 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho assert prepared.returncode == 0, prepared.stderr native_configuration = json.loads(prepared.stdout)["native_config"] automatic_output_budget = host == "benchmark" or native_configuration is not None - output_limit = reader_output_bytes or (64000 if automatic_output_budget else 7900) + native_guard_output_bytes = (7900 if not automatic_output_budget or ( + native_history_tokens is not None and native_configuration is not None + and native_history_tokens < native_configuration["config_overrides"]["tool_output_token_limit"]) + else None) + output_limit = reader_output_bytes or native_guard_output_bytes or 64000 hook_command = (native_configuration["hooks"]["SessionStart"][0]["hooks"][0]["command"] if native_configuration else shlex.join(cli + ["quota", "context-hook", "--context-delivery", "on", *host_args, context_option, str(lifecycle)])) if failed_primary: @@ -434,21 +438,30 @@ def response(request, number, message): return tool(cmd, number) def tool(cmd, number): - def bounded_read(command): - if not reader_output_bytes or "context-read" not in shlex.split(command): + if native_guard_output_bytes is not None: + argv = shlex.split(cmd) + if "should-run" in argv or "context-project" in argv: + assert "--max-page-output-bytes" not in argv + argv.extend(["--max-page-output-bytes", str(native_guard_output_bytes)]) + cmd = shlex.join(argv) + + def bounded_read(command, *, primary=False): + budget = reader_output_bytes or native_guard_output_bytes + if not budget or "context-read" not in shlex.split(command): return command # Apply the selected public reader budget to every batched read, # retaining the original capture, digest and cursor. argv = shlex.split(command) requested = argv.index("--page-bytes") + 1 - argv[requested] = str(reader_output_bytes if bounded_pages else - min(int(argv[requested]), reader_output_bytes // 2)) + page_limit = budget if bounded_pages else budget // 2 + argv[requested] = str(page_limit if primary and bounded_pages and reader_output_bytes else + min(int(argv[requested]), page_limit)) if "--max-page-output-bytes" in argv: - argv[argv.index("--max-page-output-bytes") + 1] = str(reader_output_bytes) + argv[argv.index("--max-page-output-bytes") + 1] = str(budget) else: - argv.extend(["--max-page-output-bytes", str(reader_output_bytes)]) + argv.extend(["--max-page-output-bytes", str(budget)]) return shlex.join(argv) - cmd = bounded_read(cmd) + cmd = bounded_read(cmd, primary=True) if host == "code_mode": pragma = ('// @exec: {"max_output_tokens":24000}\n' if automatic_output_budget else '// @exec: {"max_output_tokens":12000}\n' if native_pragma else "") diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index e0e48bb611..0d0daeaefe 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -333,7 +333,8 @@ def unavailable(*args, **kwargs): @pytest.mark.parametrize("provider", ["file", "sqlite"]) -def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(tmp_path, monkeypatch, provider): +@pytest.mark.parametrize("output_limit", [None, 7900]) +def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(tmp_path, monkeypatch, provider, output_limit): from loopx.control_plane.turn_driver.codex_context import codex_context_hook from test_quota_settlement_cli import _heartbeat_receipt_count @@ -349,33 +350,55 @@ def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(t root, captures = tmp_path / "context", tmp_path / "captures" root.mkdir(mode=0o700) captures.mkdir() + projection_root = tmp_path / "projected" + projection_root.mkdir(mode=0o700) event = dict(session_id="native-session", cwd=str(project), hook_event_name="SessionStart", source="startup") codex_context_hook(str(root), event) - for _ in range(2): - rc, wire = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, - "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "native-full", - "--decision-output-root", str(captures), "--context-delivery", "on", "--codex-context-dir", str(root), cwd=project) - assert rc == 0 and wire["mode"] == "full", wire - assert "Native input observation independently verifies" not in wire["instruction"] - assert "Native input observation independently verifies" not in wire["recovery"]["instruction"] - # Consume the advertised first page and continuation, not another guard. - from loopx.cli import build_parser - from loopx.cli_commands.quota_capture import read_context_capture - import shlex - page, observed = wire, b"" + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import read_context_capture + import shlex + + limit = output_limit or 64000 + def consume(page): + observed = b"" while True: + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= limit assert page["offset"] == len(observed) observed += page["content"].encode() if page["eof"]: - break + return observed args = build_parser().parse_args(shlex.split(page["next_command"])[1:]) + assert args.max_page_output_bytes == limit page = read_context_capture(args) assert page["ok"] + + for _ in range(2): + rc, wire = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "native-full", + "--decision-output-root", str(captures), "--context-delivery", "on", "--codex-context-dir", str(root), + *([] if output_limit is None else ["--max-page-output-bytes", str(output_limit)]), cwd=project) + assert rc == 0 and wire["mode"] == "full", wire + assert "Native input observation independently verifies" not in wire["instruction"] + assert "Native input observation independently verifies" not in wire["recovery"]["instruction"] + # Consume the advertised first page and continuation, not another guard. + observed = consume(wire) assert hashlib.sha256(observed).hexdigest() == wire["sha256"] assert json.loads(observed) == json.loads(Path(wire["recovery"]["path"]).read_text()) assert json.loads(observed)["heartbeat_receipt"]["turn_instance_id"] == "native-full" - assert wire["eof"] and wire["content"].encode() == observed and "context" not in wire - assert len(json.dumps(wire, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 64000 + assert "context" not in wire + if output_limit is None: + assert wire["eof"] and wire["content"].encode() == observed + else: + assert not wire["eof"] + recovery_args = build_parser().parse_args(shlex.split(wire["recovery"]["command"])[1:]) + assert recovery_args.max_page_output_bytes == limit + rc, projected = _run_cli(registry, runtime, "quota", "context-project", "--codex-app", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "native-full", + "--capture-file", wire["recovery"]["path"], "--capture-sha256", wire["recovery"]["sha256"], + "--decision-output-root", str(projection_root), "--context-delivery", "on", + "--codex-context-dir", str(root), "--max-page-output-bytes", str(limit), cwd=project) + assert rc == 0 and projected["mode"] == "full", projected + assert consume(projected) == observed assert _heartbeat_receipt_count(runtime, "native-full") == 1 assert _spend_run_count(runtime) == 0 and state.read_bytes() == original @@ -633,6 +656,16 @@ def test_native_large_pages_do_not_depend_on_shell_environment(tmp_path, monkeyp assert max(sizes) > 32000 and len(sizes) < len(original) / 20000 +@pytest.mark.parametrize("output_limit", [0, -1]) +def test_native_frame_rejects_non_positive_output_before_wire_io(tmp_path, output_limit): + from loopx.cli_commands.quota_capture import native_context_capture_view, QuotaDecisionTransportError + + with pytest.raises(QuotaDecisionTransportError): + native_context_capture_view({"requirements": "Keep the complete original."}, + tmp_path, max_page_output_bytes=output_limit) + assert not list(tmp_path.iterdir()) + + def test_default_json_reader_and_generated_recovery_use_large_pages(tmp_path): from loopx.cli import build_parser from loopx.cli_commands.quota_capture import read_context_capture From aef89bb7401e519b9de625fb6c3969eea0011197 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:28:47 +0800 Subject: [PATCH 5/6] test(context): keep the native guard pipeline intact when capping output Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 1bfee07050..21119821df 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -227,6 +227,9 @@ def invoke(name): continued += ["--turn-instance-id", original["heartbeat_receipt"]["turn_instance_id"], "--todo-id", TODO_ID] rendered = shlex.join(continued) + if native_guard_output_bytes is not None: + assert "--max-page-output-bytes" not in shlex.split(rendered) + rendered += f" --max-page-output-bytes {native_guard_output_bytes}" # Observe the producer's stdout and retain an independent exact copy; # a later cat is not the native guard's own output path. return "set -o pipefail\n" + rendered + " | tee " + shlex.quote(str(wire_paths[name])) @@ -438,13 +441,6 @@ def response(request, number, message): return tool(cmd, number) def tool(cmd, number): - if native_guard_output_bytes is not None: - argv = shlex.split(cmd) - if "should-run" in argv or "context-project" in argv: - assert "--max-page-output-bytes" not in argv - argv.extend(["--max-page-output-bytes", str(native_guard_output_bytes)]) - cmd = shlex.join(argv) - def bounded_read(command, *, primary=False): budget = reader_output_bytes or native_guard_output_bytes if not budget or "context-read" not in shlex.split(command): From 0c3c68b6f15df87d54fc5203ee0e264bd974fe97 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:33:02 +0800 Subject: [PATCH 6/6] test(context): align native frame and batch budgets with actual host limits Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/product/releases/v2.2.0.md | 4 ++++ .../test_codex_context_capture.py | 20 +++++++++++++------ .../test_context_delivery_cli.py | 12 ++++++++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/product/releases/v2.2.0.md b/docs/product/releases/v2.2.0.md index c689e9fef9..011894bd61 100644 --- a/docs/product/releases/v2.2.0.md +++ b/docs/product/releases/v2.2.0.md @@ -36,6 +36,8 @@ loopx dashboard Canonical state, complete Todo reads and shared decision ownership stay with their typed owners. Read-only status follows the canonical source runtime; runtime aliases do not create a different authority. Goal creation can resume its original operation without erasing later writes. [#6874](https://github.com/loopx-project/loopx/pull/6874), [#7325](https://github.com/loopx-project/loopx/pull/7325), [#7462](https://github.com/loopx-project/loopx/pull/7462), [#7411](https://github.com/loopx-project/loopx/pull/7411), [#7298](https://github.com/loopx-project/loopx/pull/7298), [#7381](https://github.com/loopx-project/loopx/pull/7381), [#7496](https://github.com/loopx-project/loopx/pull/7496). +An ordinary CLI self-claim now prepares execution atomically when the explicit actor matches the assignee and no explicit lease proof is supplied. Follow the returned progress/completion commands: progress can renew that still-active original execution, while expiry, replacement, foreign ownership and unavailable proof refuse without acquiring a new grant. Explicit lease calls, other assignments, soft-claim policy, read-only guards and App manual completion retain their existing authority. [#7510](https://github.com/loopx-project/loopx/pull/7510). + Replan, outcome review, terminal refusal and original settlement retain their own identities. Required Host work does not grant ordinary delivery; no-write failures preserve their original source and journal. [#7484](https://github.com/loopx-project/loopx/pull/7484), [#7486](https://github.com/loopx-project/loopx/pull/7486), [#7452](https://github.com/loopx-project/loopx/pull/7452), [#7470](https://github.com/loopx-project/loopx/pull/7470). ## Capabilities & Workflows @@ -320,6 +322,8 @@ For an existing installation, finish active work, run update check and plan, the 完整 Todo 读取与共享决策复用 typed owner,status 沿 canonical source runtime 路由,原 Goal 创建可在中断后接续,不抹掉后续写入。replan、outcome review 与原结算保留身份;必须执行 Host 不等于普通交付授权。[#6874](https://github.com/loopx-project/loopx/pull/6874), [#7325](https://github.com/loopx-project/loopx/pull/7325), [#7462](https://github.com/loopx-project/loopx/pull/7462), [#7411](https://github.com/loopx-project/loopx/pull/7411), [#7298](https://github.com/loopx-project/loopx/pull/7298), [#7381](https://github.com/loopx-project/loopx/pull/7381), [#7496](https://github.com/loopx-project/loopx/pull/7496), [#7484](https://github.com/loopx-project/loopx/pull/7484), [#7486](https://github.com/loopx-project/loopx/pull/7486), [#7452](https://github.com/loopx-project/loopx/pull/7452), [#7470](https://github.com/loopx-project/loopx/pull/7470)。 +普通 CLI 在 actor 明确等于 assignee、且未提供显式 lease proof 的自 claim 中,原子准备 execution,并返回可直接使用的 progress/completion 命令。progress 只续原来仍有效的 execution;过期、被替代、其他 holder 或 proof 不可用时显式拒绝,不自动另取授权。显式 lease、其他指派、soft-claim、只读 guard 与 App 手工完成权保持原语义。[#7510](https://github.com/loopx-project/loopx/pull/7510)。 + ### 能力与工作流 研究页先展示结果、报告与问题,再深入历史、数量与原引用;搜索和比较复用保存译文,导出原记录。新准备持久化忠实本地化指导,旧冻结方法、失败与用量保留。交付分析 producer 和打包 reader,科学校准另验。[#7051](https://github.com/loopx-project/loopx/pull/7051), [#7129](https://github.com/loopx-project/loopx/pull/7129), [#7370](https://github.com/loopx-project/loopx/pull/7370), [#7412](https://github.com/loopx-project/loopx/pull/7412), [#7423](https://github.com/loopx-project/loopx/pull/7423), [#7428](https://github.com/loopx-project/loopx/pull/7428), [#7444](https://github.com/loopx-project/loopx/pull/7444), [#7451](https://github.com/loopx-project/loopx/pull/7451)。 diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 21119821df..f6a28de082 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -243,10 +243,11 @@ def pages(name, path): # Plan only read offsets. Every actual model-facing page must # come from the public CLI, including digest verification. page_size = 3072 if adaptive or host == "code_mode" else 5000 - if reader_output_bytes and not bounded_pages: + page_output_budget = reader_output_bytes or native_guard_output_bytes + if page_output_budget and not bounded_pages: # Offset planning must match the bounded payload, with room for # the serialized page metadata and escaped source text. - page_size = min(page_size, reader_output_bytes // 2) + page_size = min(page_size, page_output_budget // 2) offsets, start, count = [0], 0, 0 for character in raw.decode(): size = len(character.encode()) @@ -349,7 +350,7 @@ def response(request, number, message): descriptor = _last_page(request) assert descriptor and descriptor["ok"], "native guard is not complete model-visible JSON" assert descriptor == json.loads(wire_paths[name].read_bytes()) - assert len(json.dumps(descriptor, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 7900 + assert len(json.dumps(descriptor, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= (native_guard_output_bytes or 64000) read_commands[name] = descriptor["command"] # The public native CLI now shares context-project's paged reader. # Keep the independent full-wire oracle separate from its page view. @@ -458,6 +459,13 @@ def bounded_read(command, *, primary=False): argv.extend(["--max-page-output-bytes", str(budget)]) return shlex.join(argv) cmd = bounded_read(cmd, primary=True) + + def tool_output_tokens(command, fallback): + if automatic_output_budget and any(part in shlex.split(command) + for part in ("context-read", "should-run", "context-project")): + return 22000 + return fallback + if host == "code_mode": pragma = ('// @exec: {"max_output_tokens":24000}\n' if automatic_output_budget else '// @exec: {"max_output_tokens":12000}\n' if native_pragma else "") @@ -469,16 +477,16 @@ def bounded_read(command, *, primary=False): commands.append(bounded_read(stages["pages"].pop(0))) if adaptive: code = "".join("{const r=await tools.exec_command(" + json.dumps({ - "cmd": command, "login": False, "max_output_tokens": 22000 if automatic_output_budget and "context-read" in shlex.split(command) else 11000 if reader_output_bytes else 2000 if bounded_pages else 2400}) + "cmd": command, "login": False, "max_output_tokens": tool_output_tokens(command, 11000 if reader_output_bytes else 2000 if bounded_pages else 2400)}) + ");text(r.exit_code === 0 ? r.output : r);}" for command in commands) return {"type": "custom_tool_call", "id": f"ctc_{number}", "call_id": f"call_{number}", "name": "exec", "input": pragma + code} return {"type": "custom_tool_call", "id": f"ctc_{number}", "call_id": f"call_{number}", "name": "exec", "input": pragma + "".join("text(await tools.exec_command(" + json.dumps({ - "cmd": command, "max_output_tokens": 2000}) + "));" for command in commands)} + "cmd": command, "max_output_tokens": tool_output_tokens(command, 2000)}) + "));" for command in commands)} return {"type": "function_call", "id": f"fc_{number}", "call_id": f"call_{number}", "name": "exec_command", "arguments": json.dumps({"cmd": cmd, - "max_output_tokens": 22000 if automatic_output_budget and "context-read" in shlex.split(cmd) else 2000})} + "max_output_tokens": tool_output_tokens(cmd, 2000)})} def usage(request, number): count = (2000000 if host == "benchmark" else 30000) if number == stages["compact_response"] else 1 diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 0d0daeaefe..6fe69c4204 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -119,13 +119,19 @@ def test_bounded_continuation_rechecks_original_capture_and_utf8_cursor(tmp_path assert "no guard, admission or spend" in result["recovery"] -def test_page_output_budget_rejects_guard_use_without_admission(tmp_path, monkeypatch): +@pytest.mark.parametrize("extra,limit,reason", [ + ([], 7000, "only supported by JSON context-read"), + (["--codex-app", "--context-delivery", "off"], 7000, "only supported by JSON context-read"), + (["--codex-app", "--context-delivery", "on"], 0, "must be positive"), + (["--codex-app", "--context-delivery", "on"], -1, "must be positive"), +]) +def test_page_output_budget_rejects_guard_use_without_admission(tmp_path, monkeypatch, extra, limit, reason): isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) code, result = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "invalid-page-budget-guard", - "--max-page-output-bytes", "7000", cwd=project) - assert code == 1 and not result["ok"] and "only supported by JSON context-read" in result["reason"] + "--max-page-output-bytes", str(limit), *extra, cwd=project) + assert code == 1 and not result["ok"] and reason in result["reason"] assert _heartbeat_receipt_count(runtime, "invalid-page-budget-guard") == 0 assert _spend_run_count(runtime) == 0