diff --git a/benchmark/runtime/RUNTIME.md b/benchmark/runtime/RUNTIME.md index 8b8936996e..223f0e9a89 100644 --- a/benchmark/runtime/RUNTIME.md +++ b/benchmark/runtime/RUNTIME.md @@ -96,8 +96,16 @@ reads, native discovery and hook IO in treatment cost, not just the delta bytes. Codex App setup uses the same product configuration renderer as the worker: `loopx --format json quota context-config --context-delivery on --codex-app --codex-context-root DIR --goal-id GOAL --agent-id AGENT`. -It returns native hooks and persistent recovery instructions, without installing -files or changing trust. A shared root isolates journals by native session; +It returns native hooks, persistent recovery instructions and native output +`config_overrides`, without installing files or changing trust. The opted-in +worker applies those same overrides automatically: 64000-byte continuation +outputs, a 20000-token history limit and a shell-scoped reader declaration. Page +zero remains within the ordinary 7900-byte guard limit. Persistent instructions +request 22000 tokens per reader result and 24000 for code-mode emission, one +directly forwarded stdout at a time. Unknown budgets retain small pages. These +limits do not prove model retention or net cost savings. Fresh off trials install +none of this configuration; rollback also removes generated output overrides. +A shared root isolates journals by native session; the trial-local single-session `--codex-context-dir` remains supported. See [heartbeat setup](../../docs/heartbeat-automation-prompt.md#opt-in-native-context-delivery) for activation, readback and rollback. Generated setup and a native diff --git a/benchmark/runtime/context_delivery.py b/benchmark/runtime/context_delivery.py index 721433a49e..77bb4c123f 100644 --- a/benchmark/runtime/context_delivery.py +++ b/benchmark/runtime/context_delivery.py @@ -102,4 +102,6 @@ def prepare_context_delivery(env: dict[str, str], home: Path) -> None: # a failed SessionStart handler cannot erase the read-only recovery route. native_args = ["-c", "hooks.state={" + ",".join(entries) + "}", "-c", "developer_instructions=" + json.dumps(configuration["developer_instructions"])] + native_args += [argument for key, value in configuration["config_overrides"].items() + for argument in ("-c", key + "=" + json.dumps(value))] env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"] = json.dumps(native_args) diff --git a/benchmark/tests/test_shared_codex_runtime.py b/benchmark/tests/test_shared_codex_runtime.py index cb692f77dd..73274ad7c3 100644 --- a/benchmark/tests/test_shared_codex_runtime.py +++ b/benchmark/tests/test_shared_codex_runtime.py @@ -1299,6 +1299,8 @@ def request(method, params): overrides = {trusted[i + 1].split("=", 1)[0]: trusted[i + 1].split("=", 1)[1] for i, value in enumerate(trusted) if value == "-c"} assert not any(key.startswith("model_provider") for key in overrides) + assert overrides["tool_output_token_limit"] == "20000" + assert json.loads(overrides["shell_environment_policy.set.LOOPX_CONTEXT_PAGE_OUTPUT_BYTES"]) == "64000" assert "CODEX_ROLLOUT_TRACE_ROOT" not in env assert not (home / "context-traces").exists() diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 0929c2a8cd..15db4e0ccd 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -476,12 +476,22 @@ loopx --format json quota context-config --context-delivery on --codex-app \ --goal-id --agent-id ``` -The result contains `native_config.hooks` and +The result contains `native_config.config_overrides`, `native_config.hooks` and `native_config.developer_instructions`. Merge the hooks into the host's existing `hooks.json` or inline hook configuration, and append the recovery instruction to -its existing developer instructions. Preserve other configuration. Review and +its existing developer instructions. Apply each flat key/value in `config_overrides` +to the native host configuration (or as a `-c key=value` override). This selects +64000-byte context continuations, a 20000-token history limit, and a shell-scoped +reader budget declaration. The initial guard stays small. Its instruction requests +22000 tokens per reader result and 24000 for code-mode emission, with one directly +forwarded stdout per call. Unconfigured hosts retain the small-page default; +the isolated benchmark worker applies this profile automatically when opted in. +Preserve other configuration. Review and trust only these exact hooks through Codex's native hook trust surface; the CLI does not install files, approve hashes, enable diagnostics or run a guard. +Rollback removes these generated output overrides along with the context hooks +and recovery instruction; restore any previous host values. The profile does not +establish model retention, task gains or net fee savings. An already loaded native thread may keep its earlier hook configuration even when `hooks/list` discovers the new handlers as enabled and trusted. That readback does not deliver `SessionStart` or initialize its context journal. After active diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index abab69730d..7b581bc770 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -255,7 +255,7 @@ Native `quota should-run` and the saved `context-project` entry share the same paged presentation. Each returns page zero, a digest-pinned continuation and full recovery without another admission. The inline page contains up to 3 KiB and shrinks to keep the complete guard stdout, including recovery and instruction fields, -within 7900 UTF-8 bytes. Continuations +within 7900 UTF-8 bytes. Without the generated native output configuration, continuations request up to 8 KiB of content with `--max-page-output-bytes 7900`, which bounds successful JSON stdout including escaping, commands and newline. Consume the inline page before following next_command; when the retained-history limit is unknown, @@ -280,7 +280,16 @@ their separate confirmed-input contract. For native setup, `quota context-config --codex-app --goal-id GOAL --agent-id AGENT` under machine on prepares only that private transport leaf and renders the -existing hooks and recovery instructions. For an SSH Goal, replace `--codex-app` +existing hooks, recovery instructions and `config_overrides`. Applying those overrides +selects 64000-byte continuation outputs and a 20000-token native history limit; +the initial guard remains within 7900 bytes. The worker applies this configuration +automatically inside its isolated opt-in trial. The persistent instruction requests +22000 tokens for each context-read tool result and 24000 for its code-mode emission, +forwarding one stdout at a time. Unknown or unsupported output declarations retain +the small-page default. Remove the generated output overrides together with the +context setup on rollback; saving a machine preference alone does not apply them. +This output sizing does not prove retained input, lower fees or task benefit. +For an SSH Goal, replace `--codex-app` with `--runtime-profile codex_app_ssh_goal` in setup and the thin/bootstrap prompt. Its generated guard keeps the native Goal identity and visible-Goal settlement; it does not become a hosted heartbeat. It does not install or trust hooks, @@ -473,11 +482,17 @@ SSH Goal 使用 `--runtime-profile codex_app_ssh_goal` 生成 context-config 和 沿用可见 Goal 的执行与结算身份;仅启用差分时接入捕获目录,不转换成自动心跳。 原生 `quota should-run` 与已保存捕获的 `context-project` 共用分页呈现,直接返回第零页、 固定摘要的后续读取与完整恢复,不另行准入。内联正文最多 3 KiB,必要时缩小,使包含 -恢复字段与完整指引的 guard stdout 不超过 7900 UTF-8 字节;后续命令最多请求 +恢复字段与完整指引的 guard stdout 不超过 7900 UTF-8 字节。未应用生成的原生输出配置时,后续命令最多请求 8 KiB 正文,并用 `--max-page-output-bytes 7900` 将成功 JSON stdout(含转义、 后续命令与换行)限制为 7900 字节。先消费内联页,再跟随 next_command;保留历史 上限未知时,每次只读一个有界输出。该上限不证明模型保留。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 +原生配置同时返回 `config_overrides`:应用后,续读输出上限为 64000 字节,原生历史 +上限为 20000 token,首次 guard 仍保持 7900 字节。开启的 benchmark worker 在其 +隔离会话内自动应用;持久指引要求每次续读直接输出预算 22000 token、Code Mode +输出预算 24000 token,每次直接转发一份 stdout。未知或不支持的输出声明保持小页。 +回滚时同时移除生成的输出覆盖项;仅保存机器偏好并不会应用这些宿主配置。 +该设置不证明输入保留、总费用降低或任务效果改善。 读取器校验不可变 capture 的摘要。Post-tool 适配层复用显式输入观察的字节范围校验, 先将实际 UTF-8 正文与可信 journal 指向的物理捕获及固定摘要核对,再合并读取范围。 正文改变、文件丢失或替换、符号链接及摘要失败均不得晋升基线;保留原始恢复引用, @@ -551,7 +566,7 @@ exact digest and Turn receipt; they never rerun admission or spend quota. With delivery on, a successful saved projection also returns page zero using the existing `context_capture_page_v1` fields. Its content is bounded to 3 KiB and shrinks against the final presentation's 7900-byte stdout limit, including -recovery, activation and instruction metadata. Subsequent native commands request up to +recovery, activation and instruction metadata. Without the generated output profile, subsequent native commands request up to 8 KiB of content within a 7900-byte successful JSON stdout limit. The optional JSON-only `--max-page-output-bytes` flag shrinks content at UTF-8 boundaries and preserves the limit in `next_command`. An insufficient limit returns an error @@ -567,7 +582,10 @@ Codex 0.162.1 qualification uses a 10,000-token history setting and a 12,000-tok code-mode result budget with one directly forwarded stdout per call. Unknown budgets keep the existing 7900-byte native default. A larger requested result alone never proves history retention; missing output still needs same-capture -recovery. This changes no host setting automatically. +recovery. The reader flag changes no host setting. The shared native setup described +above supplies the larger 64000-byte profile and its matching history/execution +budgets together; only the opted-in worker applies them automatically. A legacy +consumer that has not applied that setup keeps the existing small-page boundary. Consume `content`, then follow `next_command` through `eof` for full byte coverage. The reader verifies the original digest; do not retype or hash the content. The existing `command` still restarts at zero for older readers or incomplete inline @@ -585,7 +603,7 @@ input retention. 开启时,成功的已保存投影同时用现有 `context_capture_page_v1` 字段返回第一页。 该页内容最多 3 KiB,按最终呈现的 7900 字节 stdout 上限缩小,包含恢复、启用配置与 -完整指引元数据;后续原生命令最多请求 8 KiB +完整指引元数据;未应用生成的输出配置时,后续原生命令最多请求 8 KiB 正文,成功 JSON stdout 限制为 7900 字节。可选的 JSON 专用参数 `--max-page-output-bytes` 按 UTF-8 边界缩减正文,并在 `next_command` 中保留。 上限不足时返回错误,不返回无进展的分页;增大上限后重读同一捕获。省略该参数 @@ -595,7 +613,9 @@ input retention. 上限才能请求超过 8 KiB 的正文,最大仍受既有 capture 大小限制;上限包含转义、 JSON 外层与换行,后续命令继承该值。Codex 0.162.1 定向验证采用 10,000 token 历史 设置和 12,000 token code-mode 输出预算,每次直接转发一份 stdout。未知预算保持 -7900 字节原生默认;大输出请求不证明历史保留,缺失仍恢复同一 capture,不自动修改宿主设置。 +7900 字节原生默认;大输出请求不证明历史保留,缺失仍恢复同一 capture。 +读取参数本身不修改宿主设置。上述共享原生配置将 64000 字节分页与配套的历史和执行 +预算一起提供,仅开启的 worker 自动应用;尚未应用该配置的旧读取方保持小页边界。 先读取 `content`,再沿 `next_command` 读至 `eof`,确保完整字节覆盖。 读取器校验原始摘要;无需重抄正文或计算哈希。 原有 `command` 仍从零开始,供旧读取方或内联输出不完整时重读;可选的首屏读取 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index e8967f1824..6a77fce2f3 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -18,7 +18,8 @@ from ..control_plane.quota.error_codes import QuotaCommandValidationError, QuotaDecisionTransportError from ..control_plane.turn_driver.decision_capture import ( validate_handoff_socket, CONTEXT_CAPTURE_READ_GUIDANCE, CONTEXT_CAPTURE_PAGE_GUIDANCE, context_capture_read_command, - NATIVE_CONTEXT_PAGE_BYTES, NATIVE_CONTEXT_PAGE_OUTPUT_BYTES, + NATIVE_CONTEXT_PAGE_OUTPUT_BYTES, + native_context_page_budget, read_context_capture_file as _read_capture, ) @@ -35,7 +36,8 @@ def native_context_activation(args: argparse.Namespace) -> dict[str, Any]: def _capture_page(path: Path, raw: bytes, digest: str, *, offset: int, size: int, output_limit: int | None, instruction: bool, presentation: dict[str, Any] | None = None, - next_page_bytes: int | None = None) -> dict[str, Any]: + next_page_bytes: int | None = None, + next_page_output_bytes: int | None = None) -> dict[str, Any]: end = min(offset + size, len(raw)) while end < len(raw) and raw[end] & 0xC0 == 0x80: end -= 1 @@ -50,7 +52,7 @@ def page(text: str) -> dict[str, Any]: result["read_instruction"] = CONTEXT_CAPTURE_PAGE_GUIDANCE if cursor < len(raw): result["next_command"] = context_capture_read_command(path, digest, offset=cursor, page_bytes=next_page_bytes or size, - max_page_output_bytes=output_limit) + max_page_output_bytes=next_page_output_bytes or output_limit) return result def fits(result: dict[str, Any]) -> bool: @@ -78,7 +80,8 @@ def fits(result: dict[str, Any]) -> bool: def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, Any] | None = None, - next_page_bytes: int | None = None) -> dict[str, Any]: + next_page_bytes: int | None = None, + next_page_output_bytes: int | None = None) -> dict[str, Any]: """Bounded local IO over immutable JSON, not a new context/authority owner. Verify the complete byte digest on every page. The cursor addresses bytes @@ -120,7 +123,8 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An raise ValueError("capture offset is outside the file or not a UTF-8 boundary; use the returned next offset") return _capture_page(path, raw, digest, offset=offset, size=size, output_limit=output_limit, instruction=bool(context_dir), - presentation=presentation, next_page_bytes=next_page_bytes) + presentation=presentation, next_page_bytes=next_page_bytes, + next_page_output_bytes=next_page_output_bytes) except (OSError, ValueError, RecursionError) as exc: # Recovery is presentation-only: keep failures separate from quota's # collection error path and retain the existing receipt/capture. @@ -147,8 +151,9 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, mode, target = projected["mode"], projected["target"] if mode == "full": reference = {key: projected["recovery"][key] for key in ("path", "sha256")} + page_bytes, output_bytes = native_context_page_budget() reference["command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], - page_bytes=NATIVE_CONTEXT_PAGE_BYTES, max_page_output_bytes=NATIVE_CONTEXT_PAGE_OUTPUT_BYTES) + page_bytes=page_bytes, max_page_output_bytes=output_bytes) result = {"ok": True, **(metadata or {}), "mode": mode, **reference, **({"target": target} if target else {})} # The codec puts native continuity semantics before its change tree. Keep @@ -167,7 +172,7 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, page = read_context_capture(argparse.Namespace(capture_file=reference["path"], capture_sha256=reference["sha256"], offset=0, page_bytes=3072, format="json", max_page_output_bytes=NATIVE_CONTEXT_PAGE_OUTPUT_BYTES), - presentation=result, next_page_bytes=NATIVE_CONTEXT_PAGE_BYTES) + presentation=result, next_page_bytes=page_bytes, next_page_output_bytes=output_bytes) if page["ok"]: result = page from ..control_plane.turn_driver.codex_context import register_native_context_view @@ -482,7 +487,7 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca cli_argv=cli_argv, scenario=activation["scenario"], session_root=bool(getattr(args, "codex_context_root", None)), inherit_machine=bool(machine_runtime)) payload = {"ok": True, "context_delivery": activation, "native_config": config, - "activation": "Merge hooks and append developer_instructions to the existing native configuration; " + "activation": "Apply config_overrides, merge hooks and append developer_instructions to the existing native configuration; " "review and trust only these exact hooks in Codex. " "Existing loaded threads may retain their original hook configuration; enabled/trusted " "discovery is not session initialization or live adoption. Start/resume through the host " @@ -492,7 +497,9 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca "Pass --context-delivery on with the same declared host mode, context location and a decision-output-root to heartbeat-prompt/should-run. ") + "Normal completed full reads establish an explicit continuity assumption; no input diagnostics or provider change is required. " - "Known invalidation or incomplete reads keep full recovery. Set --context-delivery off and remove context options to restore ordinary output. " + "The generated output profile uses 64000-byte continuations and a 20000-token native history limit; " + "unknown budgets retain small pages. Known invalidation or incomplete reads keep full recovery. " + "Set --context-delivery off and remove the generated context configuration, including its output overrides, to restore ordinary output. " "This command installs nothing and grants no execution authority."} except (OSError, ValueError, TypeError) as exc: payload = {"ok": False, "reason": str(exc)} diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 564c496303..421970f57e 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -19,6 +19,8 @@ write_decision_capture, capture_context_projection, project_captured_context, CONTEXT_CAPTURE_PAGE_GUIDANCE, capture_page_range, read_context_capture_file, context_capture_read_command, NATIVE_CONTEXT_PAGE_BYTES, NATIVE_CONTEXT_PAGE_OUTPUT_BYTES, + NATIVE_CONTEXT_LARGE_OUTPUT_BYTES, NATIVE_CONTEXT_HISTORY_TOKENS, NATIVE_CONTEXT_OUTPUT_ENV, + native_context_page_budget, ) # Native adapter activation reuses scheduler-owned host declarations. These @@ -219,17 +221,23 @@ def _context_continuation_instructions(command: str) -> str: def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_id: str, - cli_argv: list[str], session_root: bool = False) -> str: + cli_argv: list[str], session_root: bool = False, + page_output_bytes: int = NATIVE_CONTEXT_PAGE_OUTPUT_BYTES) -> str: """Host-owned persistent instructions, independent of event-hook delivery.""" import shlex command = shlex.join([*cli_argv, "--format", "json", "quota", "context-read", "--codex-context-root" if session_root else "--codex-context-dir", directory, - "--goal-id", goal_id, "--agent-id", agent_id, "--page-bytes", str(NATIVE_CONTEXT_PAGE_BYTES), - "--max-page-output-bytes", str(NATIVE_CONTEXT_PAGE_OUTPUT_BYTES)]) + "--goal-id", goal_id, "--agent-id", agent_id, "--page-bytes", str(max(NATIVE_CONTEXT_PAGE_BYTES, page_output_bytes)), + "--max-page-output-bytes", str(page_output_bytes)]) return ( _context_continuation_instructions(command) + - "Follow the returned next_command through eof; verify complete bytes and digest. " + "Follow the returned next_command through eof; verify complete bytes and digest. " + + ("The generated native configuration supports 64000-byte context-read outputs and a 20000-token history limit. " + "Read one continuation per tool call with max_output_tokens=22000. In code-mode, also set the exec " + "pragma max_output_tokens to 24000 and forward successful stdout directly with text(result.output). " + "Keep the initial guard result small; these larger budgets apply to context-read calls only. " + if page_output_bytes == NATIVE_CONTEXT_LARGE_OUTPUT_BYTES else "") + "For code-mode reads, size batches against both direct-result and retained-history output limits; " "max_output_tokens alone does not establish the latter. Read one output at a time with " "--max-page-output-bytes when the effective history limit is unknown; for legacy default pages, " @@ -264,11 +272,15 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, option, directory]), "timeout": 10} # Native Interrupt handlers have a 3s ceiling. Declare the effective value # rather than letting discovery silently clamp a different configuration. - return {"hooks": {event: [{**({"matcher": "^Bash$"} if event == "PostToolUse" else {}), + return {"config_overrides": { + "tool_output_token_limit": NATIVE_CONTEXT_HISTORY_TOKENS, + "shell_environment_policy.set." + NATIVE_CONTEXT_OUTPUT_ENV: str(NATIVE_CONTEXT_LARGE_OUTPUT_BYTES)}, + "hooks": {event: [{**({"matcher": "^Bash$"} if event == "PostToolUse" else {}), "hooks": [{**handler, "timeout": 3} if event == "Interrupt" else handler]}] for event in CONTEXT_LIFECYCLE_HOOKS}, "developer_instructions": codex_context_recovery_instructions(directory, goal_id=goal_id, - agent_id=agent_id, cli_argv=cli_argv, session_root=session_root)} + agent_id=agent_id, cli_argv=cli_argv, session_root=session_root, + page_output_bytes=NATIVE_CONTEXT_LARGE_OUTPUT_BYTES)} def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str, Any]: @@ -470,8 +482,9 @@ def project_codex_context(payload: dict[str, Any], *, directory: str | None, cap request = _native_context_request(state, scope) recovery = capture_context_projection(payload, directory=capture_directory, read_guidance=CONTEXT_CAPTURE_PAGE_GUIDANCE) + page_bytes, output_bytes = native_context_page_budget() recovery["command"] = context_capture_read_command(Path(recovery["path"]), recovery["sha256"], - page_bytes=NATIVE_CONTEXT_PAGE_BYTES, max_page_output_bytes=NATIVE_CONTEXT_PAGE_OUTPUT_BYTES) + page_bytes=page_bytes, max_page_output_bytes=output_bytes) state["recovery"] = {**{key: recovery[key] for key in ("path", "sha256", "command")}, "goal_id": goal_id, "agent_id": agent_id, "total_bytes": Path(recovery["path"]).stat().st_size} diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index e127e6a16b..62a4285412 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -137,6 +137,17 @@ def validate_handoff_socket(value: str) -> Path: # This does not establish downstream retained-history capacity or retention. NATIVE_CONTEXT_PAGE_BYTES = 8192 NATIVE_CONTEXT_PAGE_OUTPUT_BYTES = 7900 +# Installed together by the opt-in native configuration. Unknown hosts retain +# the small-page boundary; this declaration is output capacity, not retention. +NATIVE_CONTEXT_LARGE_OUTPUT_BYTES = 64000 +NATIVE_CONTEXT_HISTORY_TOKENS = 20000 +NATIVE_CONTEXT_OUTPUT_ENV = "LOOPX_CONTEXT_PAGE_OUTPUT_BYTES" + + +def native_context_page_budget() -> tuple[int, int]: + if os.environ.get(NATIVE_CONTEXT_OUTPUT_ENV) == str(NATIVE_CONTEXT_LARGE_OUTPUT_BYTES): + return NATIVE_CONTEXT_LARGE_OUTPUT_BYTES, NATIVE_CONTEXT_LARGE_OUTPUT_BYTES + return NATIVE_CONTEXT_PAGE_BYTES, NATIVE_CONTEXT_PAGE_OUTPUT_BYTES def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, page_bytes: int = 4096, diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index a78fc59438..32178814ad 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -130,6 +130,8 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho capture_output=True, text=True, timeout=15) 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) 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: @@ -221,7 +223,9 @@ def invoke(name): continued += ["--turn-instance-id", original["heartbeat_receipt"]["turn_instance_id"], "--todo-id", TODO_ID] rendered = shlex.join(continued) - return rendered + " > " + shlex.quote(str(wire_paths[name])) + " && cat " + shlex.quote(str(wire_paths[name])) + # 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])) def pages(name, path): raw = path.read_bytes() @@ -268,6 +272,12 @@ def pages(name, path): def delivered(request, name): return _delivered(request, page_digests[name]) + def native_state(): + from loopx.control_plane.turn_driver.codex_context import _state + directory = (lifecycle / hashlib.sha256(wires["warm"]["target"]["session_id"].encode()).hexdigest() + if shared_root else lifecycle) + return _state(directory) + def response(request, number, message): nonlocal command, root phase = stages["phase"] @@ -275,9 +285,9 @@ def response(request, number, message): page = _last_page(request) assert page and page["ok"], page bounded_sizes.append(len(page["content"].encode())) - assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= (reader_output_bytes or 7900) + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= output_limit if not page["eof"]: - assert f"--max-page-output-bytes {reader_output_bytes or 7900}" in page["next_command"] + assert f"--max-page-output-bytes {output_limit}" in page["next_command"] return tool(page["next_command"], number) if stages["pages"]: cmd = stages["pages"].pop(0) @@ -403,10 +413,8 @@ def response(request, number, message): # The actual host has exited and reconstructed history again. Its # model input must retain the recovered full body, and SessionStart # must preserve that new baseline without another recovery read. - from loopx.control_plane.turn_driver.codex_context import _state - assert delivered(request, "recovery") == Path(wires["warm"]["recovery"]["path"]).read_text().rstrip("\n") - journal = _state(lifecycle) + journal = native_state() assert journal["candidate"]["target"]["context_epoch"] == stages["recovered_epoch"] assert journal["candidate"]["capture_sha256"] == page_digests["recovery"] stages["phase"] = "done" @@ -416,9 +424,7 @@ def response(request, number, message): recovered = delivered(request, "recovery") assert recovered == Path(wires["warm"]["recovery"]["path"]).read_text().rstrip("\n") if recovery_resume: - from loopx.control_plane.turn_driver.codex_context import _state - - stages["recovered_epoch"] = _state(lifecycle)["context_epoch"] + stages["recovered_epoch"] = native_state()["context_epoch"] stages["phase"] = "recovery_resuming" if recovery_resume else "done" return message return tool(cmd, number) @@ -432,7 +438,8 @@ def tool(cmd, number): argv[argv.index("--max-page-output-bytes") + 1] = str(reader_output_bytes) cmd = shlex.join(argv) if host == "code_mode": - pragma = '// @exec: {"max_output_tokens":12000}\n' if native_pragma else "" + pragma = ('// @exec: {"max_output_tokens":24000}\n' if automatic_output_budget else + '// @exec: {"max_output_tokens":12000}\n' if native_pragma else "") # Exercise the shipped default page size and bounded batching for # both output forms. One model round per page can exhaust the # unchanged native turn deadline before same-capture recovery ends. @@ -441,7 +448,7 @@ def tool(cmd, number): commands.append(stages["pages"].pop(0)) if adaptive: code = "".join("{const r=await tools.exec_command(" + json.dumps({ - "cmd": command, "login": False, "max_output_tokens": 11000 if reader_output_bytes else 2000 if bounded_pages else 2400}) + "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}) + ");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} @@ -449,7 +456,8 @@ def tool(cmd, number): "name": "exec", "input": pragma + "".join("text(await tools.exec_command(" + json.dumps({ "cmd": command, "max_output_tokens": 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": 2000})} + "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})} def usage(request, number): count = (2000000 if host == "benchmark" else 30000) if number == stages["compact_response"] else 1 @@ -474,7 +482,10 @@ def observed_response(request, number, message): config.write_text(f"tool_output_token_limit={native_history_tokens}\n" + config.read_text()) if native_configuration: config = home / "config.toml" - config.write_text("developer_instructions=" + json.dumps(native_configuration["developer_instructions"]) + config.write_text("".join(key + "=" + json.dumps(value) + "\n" + for key, value in native_configuration["config_overrides"].items() + if key != "tool_output_token_limit" or native_history_tokens is None) + + "developer_instructions=" + json.dumps(native_configuration["developer_instructions"]) + "\n" + config.read_text()) elif hook_loss: from loopx.control_plane.turn_driver.codex_context import codex_context_recovery_instructions @@ -716,6 +727,14 @@ def test_native_compact_recovery_survives_process_resume(tmp_path, monkeypatch, "code_mode", surface, False, True, True, bounded_pages=True, recovery_resume=True) +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_generated_large_page_configuration(tmp_path, monkeypatch, surface): + # Apply only the public setup output: no manual reader or history override. + test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, + "code_mode", surface, False, True, True, shared_root=True, + bounded_pages=True, native_pragma=True, native_read_batch=1, recovery_resume=True) + + @pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) @pytest.mark.parametrize("loss", ["replace", "unqualified_profile"]) def test_native_worker_resume_history_change_reads_current_full(tmp_path, monkeypatch, surface, loss): @@ -759,7 +778,7 @@ def test_native_bounded_command_chain_recovers_with_unknown_history_limit(tmp_pa # Follow only public returned commands, one stdout per call, at the existing # native deadline and unknown-model history budget; no larger host limit. test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, - "code_mode", surface, False, True, True, "missing", shared_root=True, + "code_mode", surface, False, True, True, "missing", native_read_batch=1, bounded_pages=True) diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 37cb573a56..c498c2678e 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -71,6 +71,11 @@ def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_p assert "ROLLOUT_TRACE" not in json.dumps(payload) assert "--codex-context-root" in native["developer_instructions"] assert "context-read" in native["developer_instructions"] + assert native["config_overrides"] == { + "tool_output_token_limit": 20000, + "shell_environment_policy.set.LOOPX_CONTEXT_PAGE_OUTPUT_BYTES": "64000"} + assert "--max-page-output-bytes 64000" in native["developer_instructions"] + assert "max_output_tokens=22000" in native["developer_instructions"] assert "hooks.state" not in native and list(tmp_path.iterdir()) == [] missing_identity = subprocess.run(command[:-2], cwd=tmp_path, env={**os.environ, "PYTHONPATH": str(repo)}, capture_output=True, text=True, timeout=15) diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 508b30216f..9f7098f84b 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -597,3 +597,32 @@ def test_larger_pages_need_explicit_output_budget_and_preserve_every_byte(tmp_pa assert not read_context_capture(args)["ok"] args.max_page_output_bytes = 7900 assert not read_context_capture(args)["ok"] + + +@pytest.mark.parametrize("declaration,limit", [(None, 7900), ("64000", 64000), ("999999", 7900), ("unknown", 7900)]) +def test_native_configuration_budget_keeps_guard_small_and_continuations_complete(tmp_path, monkeypatch, declaration, limit): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import native_context_capture_view, read_context_capture + + if declaration is None: + monkeypatch.delenv("LOOPX_CONTEXT_PAGE_OUTPUT_BYTES", raising=False) + else: + monkeypatch.setenv("LOOPX_CONTEXT_PAGE_OUTPUT_BYTES", declaration) + page = native_context_capture_view({"requirements": '汉🙂"\\\t\n' * 12000, + "last": "Stop before publication."}, tmp_path) + original = Path(page["path"]).read_bytes() + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 7900 + collected, sizes = b"", [] + while True: + assert page["ok"] and page["offset"] == len(collected) + collected += page["content"].encode() + sizes.append(len(page["content"].encode())) + if page["eof"]: + break + args = build_parser().parse_args(shlex.split(page["next_command"])[1:]) + assert args.max_page_output_bytes == limit + page = read_context_capture(args) + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= limit + assert collected == original and hashlib.sha256(collected).hexdigest() == page["sha256"] + if declaration == "64000": + assert max(sizes) > 32000 and len(sizes) < len(original) / 20000