From c639bde7bd2fd1610096251d13c61f7436efcb30 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:31:03 +0800 Subject: [PATCH 01/40] fix(context): keep native OpenAI inputs observable for differential delivery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/runtime/context_delivery.py | 16 +++++++++--- benchmark/tests/test_shared_codex_runtime.py | 16 ++++++++++-- docs/reference/required-work-context.md | 18 ++++++++++++- loopx/cli_commands/quota_capture.py | 4 ++- .../turn_driver/codex_context.py | 26 +++++++++++++++++-- .../test_codex_context_lifecycle.py | 15 +++++++++++ 6 files changed, 86 insertions(+), 9 deletions(-) diff --git a/benchmark/runtime/context_delivery.py b/benchmark/runtime/context_delivery.py index b2eb27f023..f05e2a4981 100644 --- a/benchmark/runtime/context_delivery.py +++ b/benchmark/runtime/context_delivery.py @@ -38,7 +38,9 @@ def preflight_context_instruction(env: dict[str, str], path: Path, digest: str, def prepare_context_delivery(env: dict[str, str], home: Path) -> None: - from loopx.control_plane.turn_driver.codex_context import codex_context_configuration + from loopx.control_plane.turn_driver.codex_context import ( + codex_context_configuration, codex_context_transport_configuration, + ) from loopx.control_plane.turn_driver.codex_stdio import StdioNativeGoalTransport root = home / "loopx-context" @@ -79,6 +81,8 @@ def prepare_context_delivery(env: dict[str, str], home: Path) -> None: native.request("initialize", {"clientInfo": {"name": "loopx_context_transport", "version": "0.1.0"}, "capabilities": {"experimentalApi": True}}) native.notify("initialized", {}) + effective = native.request("config/read", {"cwd": env["LOOPX_PROJECT"], "includeLayers": False}) + transport = codex_context_transport_configuration(effective["config"]) listed = native.request("hooks/list", {"cwds": [env["LOOPX_PROJECT"]]}) hooks = [hook for entry in listed["data"] for hook in entry["hooks"] if hook.get("sourcePath") == str(path.resolve())] @@ -94,5 +98,11 @@ def prepare_context_delivery(env: dict[str, str], home: Path) -> None: # Carry the entire table as TOML so native keys remain literal strings. # This isolated trial's native developer instructions survive compaction; # a failed SessionStart handler cannot erase the read-only recovery route. - env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"] = json.dumps(["-c", "hooks.state={" + ",".join(entries) + "}", - "-c", "developer_instructions=" + json.dumps(configuration["developer_instructions"])]) + native_args = ["-c", "hooks.state={" + ",".join(entries) + "}", + "-c", "developer_instructions=" + json.dumps(configuration["developer_instructions"])] + if transport: + provider = transport["model_provider"] + native_args += ["-c", "model_provider=" + json.dumps(provider)] + for key, value in transport["model_providers"][provider].items(): + native_args += ["-c", f"model_providers.{provider}.{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 09ec53859e..7a5b4f335c 100644 --- a/benchmark/tests/test_shared_codex_runtime.py +++ b/benchmark/tests/test_shared_codex_runtime.py @@ -1166,7 +1166,8 @@ def test_context_setup_preserves_unsafe_or_unowned_inputs(tmp_path, kind): assert "LOOPX_CODEX_CONTEXT_TRUST_ARGS" not in env -def test_context_discovery_keeps_solver_configuration_and_unrelated_hooks(tmp_path, monkeypatch): +@pytest.mark.parametrize("provider", ["openai", "operator-provider"]) +def test_context_discovery_keeps_solver_configuration_and_unrelated_hooks(tmp_path, monkeypatch, provider): from contextlib import contextmanager from benchmark.runtime.context_delivery import prepare_context_delivery from loopx.control_plane.turn_driver.codex_stdio import StdioNativeGoalTransport @@ -1185,6 +1186,8 @@ def request(method, params): methods.append(method) if method == "initialize": return {} + if method == "config/read": + return {"config": {"model_provider": provider}} assert method == "hooks/list", "discovery must not start model work" own = [{"sourcePath": str((home / "hooks.json").resolve()), "key": f"own-{i}", "currentHash": f"native-hash-{i}"} for i in range(2)] @@ -1196,7 +1199,7 @@ def request(method, params): env = {"LOOPX_CLI": "fixture-loopx", "LOOPX_GOAL_ID": "fixture", "LOOPX_AGENT_ID": "agent", "CODEX_BIN": "native-codex", "LOOPX_PROJECT": str(tmp_path)} prepare_context_delivery(env, home) - assert methods == ["initialize", "hooks/list"] + assert methods == ["initialize", "config/read", "hooks/list"] assignments = {commands[0][i + 1] for i, value in enumerate(commands[0]) if value == "-c"} assert assignments == {"features.plugins=false", "features.remote_plugin=false", "features.plugin_sharing=false"} trusted = json.loads(env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"]) @@ -1205,6 +1208,15 @@ def request(method, params): assert all("features.plugins" not in value and "features.remote_plugin" not in value and "features.plugin_sharing" not in value for value in trusted) assert config.read_bytes() == original_config + overrides = {trusted[i + 1].split("=", 1)[0]: trusted[i + 1].split("=", 1)[1] + for i, value in enumerate(trusted) if value == "-c"} + if provider == "openai": + selected = json.loads(overrides["model_provider"]) + assert json.loads(overrides[f"model_providers.{selected}.supports_websockets"]) is False + assert json.loads(overrides[f"model_providers.{selected}.requires_openai_auth"]) is True + assert not any(key.endswith(("base_url", "env_key")) for key in overrides) + else: + assert not any(key.startswith("model_provider") for key in overrides) @pytest.mark.parametrize("value", [True, False, "1", "codex_benchmark_heartbeat", "codex_app_heartbeat", "all"]) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index b11c4787cb..e4d37f46c2 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -215,7 +215,17 @@ existing hooks and recovery instructions. It does not install or trust hooks, change host configuration or enable diagnostics in a running parent. The native process still needs its exact reviewed hooks and `CODEX_ROLLOUT_TRACE_ROOT` configured before startup. Without current-input retention proof, delivery stays -full. Generated machine-inherited hooks reload the same machine runtime, so off +full. Native implicit-history requests with `previous_response_id` are not retention +proof for this observer. The rendered default OpenAI configuration therefore uses +a native HTTP provider, keeping Codex authentication and routing +without an endpoint or credential override. Benchmark preparation reads effective +native configuration before applying it; explicit operator providers are preserved, +and a conflicting provider id is rejected. A manual App setup must also preserve +an explicit provider and verify its actual input. HTTP transport and diagnostics +have costs which remain part of the comparison; selecting them does not prove a +delta or a performance gain. Benchmark overrides are per native invocation; a manual +App setup restores its previous provider when removing this configuration. +Generated machine-inherited hooks reload the same machine runtime, so off or removal stops their journal writes. Explicit manual on options remain explicit. Generate a persistent entrypoint with `heartbeat-prompt --bootstrap`. It retains @@ -319,6 +329,12 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 机器开启后,`quota context-config --codex-app --goal-id GOAL --agent-id AGENT` 只准备 私有传输末级目录并生成已有 hook 和恢复指令,不安装或信任 hook,也不更改宿主配置。 +带 `previous_response_id` 的原生隐式历史请求不能给当前观察器提供完整保留证明。 +默认 OpenAI 配置模板因此使用进程内的原生 HTTP provider,保留 Codex 登录和路由, +不改端点或凭证。Benchmark 准备会读取实际原生配置;显式自定义 provider 保持原样, +名称冲突则拒绝。手工配置 App 也须保留显式 provider,并验证实际输入。HTTP 与诊断 +成本仍须计入比较,选择它们不代表已经产生差分或性能收益。Benchmark 覆盖只作用于 +本次原生调用;手工 App 配置撤回时也须恢复此前的 provider。 原生进程启动前仍须配置已审阅的精确 hook 与 `CODEX_ROLLOUT_TRACE_ROOT`;无法证明当前 输入保留时返回完整捕获。由机器继承生成的 hook 每次重读同一个 runtime,关闭或移除 配置后停止 journal 写入;手工显式 on 保留其显式优先级。 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 4a3d9888c6..6616f11054 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -420,7 +420,9 @@ 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": "For the default OpenAI provider, merge the rendered native HTTP provider to make complete inputs observable; " + "preserve an explicit operator provider and verify its actual input separately. " + "Merge hooks and append developer_instructions to the existing native configuration; " "review and trust only these exact hooks in Codex. Restart/resume with the resulting configuration. " + ("Machine defaults are re-read on each invocation; generated hooks inherit the same machine runtime. " "Omit per-command activation and location flags to retain that inheritance. " if machine_runtime else diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 37350f4cb4..146b73b169 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -7,7 +7,7 @@ from __future__ import annotations from contextlib import contextmanager -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from typing import Any import json import os @@ -218,6 +218,25 @@ def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_i ) +def codex_context_transport_configuration(native_config: Mapping[str, Any]) -> dict[str, Any]: + """Keep the default OpenAI request observable without changing its login. + + Native WebSocket requests may carry only previous_response_id and new + items. The input observer deliberately cannot certify that implicit history. + A separate native HTTP provider retains Codex's OpenAI routing and auth; + explicit operator providers are left alone. This config grants no authority + and does not assert that any particular input was complete. + """ + if native_config.get("model_provider", "openai") != "openai": + return {} + provider = "loopx_context_http" + if provider in native_config.get("model_providers", {}): + raise ValueError("context HTTP provider conflicts with existing native configuration") + return {"model_provider": provider, "model_providers": {provider: { + "name": "OpenAI", "wire_api": "responses", "requires_openai_auth": True, + "supports_websockets": False}}} + + def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, cli_argv: list[str], scenario: str, session_root: bool = False, inherit_machine: bool = False) -> dict[str, Any]: @@ -225,6 +244,8 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, This renders no trust hashes and writes no host configuration. The native host remains responsible for reviewing/trusting the exact discovered hook. + This manual template assumes the default OpenAI provider. Automatic callers + resolve the effective provider with codex_context_transport_configuration. """ import shlex @@ -240,7 +261,8 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, *(["--codex-app"] if scenario == "codex_app_heartbeat" else ["--runtime-profile", "generic_cli"]), option, directory]), "timeout": 10} - return {"hooks": {event: [{"hooks": [handler]}] for event in ("SessionStart", "PreToolUse")}, + return {**codex_context_transport_configuration({}), + "hooks": {event: [{"hooks": [handler]}] for event in ("SessionStart", "PreToolUse")}, "developer_instructions": codex_context_recovery_instructions(directory, goal_id=goal_id, agent_id=agent_id, cli_argv=cli_argv, session_root=session_root)} diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index bb84d98551..6c31a5778e 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -73,6 +73,21 @@ def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_p assert missing_identity.returncode == 1 and not json.loads(missing_identity.stdout)["ok"] +def test_context_transport_retains_native_auth_and_explicit_provider_ownership(): + original = {"model_provider": "operator", "model_providers": {"operator": { + "base_url": "https://example.invalid", "supports_websockets": True}}} + assert context.codex_context_transport_configuration(original) == {} + assert original["model_providers"]["operator"]["supports_websockets"] is True + native = context.codex_context_transport_configuration({"model_provider": "openai"}) + selected = native["model_provider"] + assert native["model_providers"][selected] == { + "name": "OpenAI", "wire_api": "responses", "requires_openai_auth": True, + "supports_websockets": False} + with pytest.raises(ValueError, match="conflicts"): + context.codex_context_transport_configuration({"model_providers": { + selected: {"base_url": "https://example.invalid"}}}) + + def setup(root, monkeypatch): root.chmod(0o700) monkeypatch.chdir(root) From 512513e2a00f65cde523776bebca7dc48ac147d3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:42:18 +0800 Subject: [PATCH 02/40] refactor(context): share paged native reads with the ordinary quota entry Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 12 ++++ loopx/cli_commands/quota.py | 11 +++- loopx/cli_commands/quota_capture.py | 58 ++++++++++--------- .../turn_driver/decision_capture.py | 3 +- .../test_codex_context_capture.py | 12 ++-- .../test_context_delivery_cli.py | 22 ++++++- 6 files changed, 84 insertions(+), 34 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index e4d37f46c2..8ced7f6fcf 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -209,6 +209,13 @@ projection path and does not acquire a second native journal. Unrelated quota commands and digest-bound recovery pages do not activate this setting. Ordinary machine validation still applies to business operations. +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. Native pages stay at 3 KiB throughout that +read, reducing output-wrapper truncation; consume the inline page before following +next_command. Forward exec stdout directly in code mode. Repeating the packet in +a hashing tool call does not establish that its original bytes were retained. + 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. It does not install or trust hooks, @@ -329,6 +336,11 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 机器开启后,`quota context-config --codex-app --goal-id GOAL --agent-id AGENT` 只准备 私有传输末级目录并生成已有 hook 和恢复指令,不安装或信任 hook,也不更改宿主配置。 +原生 `quota should-run` 与已保存捕获的 `context-project` 共用分页呈现,直接返回第零页、 +固定摘要的后续读取与完整恢复,不另行准入。该原生读取链的页大小保持 3 KiB,以减少 +输出包装造成的截断;先消费已附带页,再跟随 next_command。Code Mode 直接转发 +exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 + 带 `previous_response_id` 的原生隐式历史请求不能给当前观察器提供完整保留证明。 默认 OpenAI 配置模板因此使用进程内的原生 HTTP provider,保留 Codex 登录和路由, 不改端点或凭证。Benchmark 准备会读取实际原生配置;显式自定义 provider 保持原样, diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index 84e110a174..ac26634cb6 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -8,7 +8,7 @@ from .quota_capture import ( bind_capture_selection_transport, capture_decision, handoff_decision, - handle_native_context_command, handle_replan_view, prepare_quota_capture, + handle_native_context_command, handle_replan_view, prepare_quota_capture, native_context_capture_view, ) from ..control_plane.turn_driver.decision_capture import project_captured_context from ..usage_goal import observe_quota_result @@ -539,6 +539,15 @@ def _emit_quota_result( capture_directory=capture_directory, goal_id=decision_identity[0], agent_id=decision_identity[1], surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet", capture_id=capture_id) + from ..control_plane.turn_driver.decision_capture import CONTEXT_CAPTURE_READ_GUIDANCE + + recovery = payload.get("recovery") + view = native_context_capture_view(payload, capture_directory) + payload = {"ok": True, **view, "capture_sha256": capture_id, + **({"recovery": recovery} if recovery else {}), + "instruction": "Presentation of this original decision only; authority and freshness are unchanged. " + + ("This result includes page zero; consume it before next_command. " if "content" in view else "") + + CONTEXT_CAPTURE_READ_GUIDANCE} 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 6616f11054..ae39eb2a63 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -104,6 +104,31 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: "recovery": "Restore the original digest-bound capture; no guard, admission or spend was performed."} +def native_context_capture_view(projected: dict[str, Any], directory: Path) -> 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 + existing digest-checked reader; never collect or admit another decision. + """ + from ..control_plane.turn_driver.decision_capture import write_decision_capture + + wire = directory / "delivery.json" + write_decision_capture(wire, projected) + reference = {"path": str(wire), "sha256": hashlib.sha256(wire.read_bytes()).hexdigest()} + mode, target = "full", None + if projected.get("schema_version") == "context_delivery_v1": + mode, target = projected["mode"], projected["target"] + if mode == "full": + reference = {key: projected["recovery"][key] for key in ("path", "sha256")} + reference["command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], page_bytes=3072) + result = {"mode": mode, **reference, **({"target": target} if target else {})} + page = read_context_capture(argparse.Namespace(capture_file=reference["path"], + capture_sha256=reference["sha256"], offset=0, page_bytes=3072)) + if page["ok"]: + result.update(page) + return result + + def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: """Present an immutable already-decided quota capture, without authority IO. @@ -137,9 +162,9 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: mode = "full" target = None native_prepared = False + native_view = None try: from ..control_plane.turn_driver.codex_context import codex_context_directory, project_codex_context - from ..control_plane.turn_driver.decision_capture import write_decision_capture if not activation["enabled"]: raise ValueError("context delivery is off") @@ -162,18 +187,9 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: goal_id=args.goal_id, agent_id=args.agent_id, surface="quota_packet", capture_id=hashlib.sha256(raw).hexdigest()) native_prepared = True - wire = cache / "delivery.json" - write_decision_capture(wire, projected) - digest = hashlib.sha256(wire.read_bytes()).hexdigest() - reference = {"path": str(wire), "sha256": digest, "command": context_capture_read_command(wire, digest)} - if projected.get("schema_version") == "context_delivery_v1": - mode = projected["mode"] - target = projected["target"] - if mode == "full": - # The native observer verifies paged complete context bytes. - # Deliver that exact full body plus its target in this small - # descriptor, rather than paging another wrapper around it. - reference = {key: projected["recovery"][key] for key in ("path", "sha256", "command")} + native_view = native_context_capture_view(projected, cache) + 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: if isinstance(exc, QuotaDecisionTransportError) or (activation["enabled"] and not native_prepared): if not isinstance(exc, QuotaDecisionTransportError): @@ -196,19 +212,9 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: **({"context_delivery": activation} if activation["enabled"] else {}), "instruction": "Presentation of the original decision only; authority and freshness are unchanged. " + CONTEXT_CAPTURE_READ_GUIDANCE} - if activation["enabled"] and native_prepared: - # Reuse the public page protocol and its physical-file/digest/UTF-8 - # checks. Older callers can still restart with command; current callers - # consume this page and avoid a descriptor-only model round trip. - # Reserve room for the descriptor in the same tool response; subsequent - # reads retain the ordinary page size and exact UTF-8 cursor. - page = read_context_capture(argparse.Namespace(capture_file=reference["path"], - capture_sha256=reference["sha256"], offset=0, page_bytes=3072)) - if page["ok"]: - if not page["eof"]: - page["next_command"] = context_capture_read_command(Path(reference["path"]), - reference["sha256"], offset=page["next_offset"]) - result.update(page) + if native_view is not None: + result.update(native_view) + if "content" in native_view: result["instruction"] = ("This result includes page zero. Consume its content, then follow " "next_command through eof. command restarts at zero if the inline page is incomplete " "or unsupported. " + result["instruction"]) diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 90f1a4bc7e..74ffbcb082 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -103,7 +103,8 @@ def validate_handoff_socket(value: str) -> Path: CONTEXT_CAPTURE_READ_GUIDANCE = ( "Follow next_command through eof and combine content in byte-offset order. " "In code-mode, forward successful exec stdout directly with text(result.output); " - "retain errors. Small batches can save model round trips; start with at most two default pages " + "retain errors. Do not retype the packet into a hashing command; that is not current-input retention proof. " + "Small batches can save model round trips; start with at most two default pages " "and inspect every output. A host may truncate inside valid JSON or omit a whole output. " "Check each model-visible content's UTF-8 byte length against next_offset (total_bytes at eof), " "complete coverage and the original SHA-256. If incomplete, reread the affected range " diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index c1f624dbcc..b8444167ba 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -239,11 +239,15 @@ def response(request, number, message): cmd = invoke(name) elif phase in {"cold_ready", "warm_ready"}: name = phase.split("_")[0] - wires[name] = json.loads(wire_paths[name].read_text()) - assert wires[name]["mode"] == ("full" if name == "cold" else "delta") + descriptor = json.loads(wire_paths[name].read_text()) + # The public native CLI now shares context-project's paged reader. + # Keep the independent full-wire oracle separate from its page view. + wire_file = Path(descriptor["path"]).parent / "delivery.json" + wires[name] = json.loads(wire_file.read_text()) + assert descriptor["mode"] == wires[name]["mode"] == ("full" if name == "cold" else "delta") capture = Path(wires[name]["recovery"]["path"]).parent / "decision.json" snapshots.append((capture, capture.read_bytes())) - read_paths[name] = Path(wires[name]["recovery"]["path"]) if name == "cold" else wire_paths[name] + read_paths[name] = Path(wires[name]["recovery"]["path"]) if name == "cold" else wire_file stages["pages"] = pages(name, read_paths[name]) stages["phase"] = name + "_read" cmd = stages["pages"].pop(0) @@ -258,7 +262,7 @@ def response(request, number, message): cmd = invoke("warm") elif phase == "warm_read": assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") - assert delivered(request, "warm") == wire_paths["warm"].read_text().rstrip("\n") + assert delivered(request, "warm") == read_paths["warm"].read_text().rstrip("\n") stages.update(phase="compact", compact_response=number) if hook_loss: loss_marker.touch() diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 1850f69aab..258a27aacc 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -5,6 +5,7 @@ from __future__ import annotations from copy import deepcopy +import hashlib import json import os from pathlib import Path @@ -248,7 +249,23 @@ def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(t "--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 json.loads(Path(wire["recovery"]["path"]).read_text()) == wire["context"] + # 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"" + while True: + assert page["offset"] == len(observed) + observed += page["content"].encode() + if page["eof"]: + break + args = build_parser().parse_args(shlex.split(page["next_command"])[1:]) + page = read_context_capture(args) + assert page["ok"] + 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 len(wire["content"].encode()) <= 3072 and "context" not in wire assert _heartbeat_receipt_count(runtime, "native-full") == 1 assert _spend_run_count(runtime) == 0 and state.read_bytes() == original @@ -294,7 +311,8 @@ def unavailable_directory(**kwargs): assert printed[0]["heartbeat_receipt"] == original["heartbeat_receipt"] assert printed[0]["captured_decision"] == {"path": str(tmp_path / "decision.json")} else: - assert printed == [{"native_projection": original}] + assert len(printed) == 1 and printed[0]["eof"] is True + assert json.loads(printed[0]["content"]) == {"native_projection": original} assert json.loads((tmp_path / "decision.json").read_text()) == original From a850523e139a1a2473757815c2ef82df925f7eef Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:44:08 +0800 Subject: [PATCH 03/40] test(context): read native fixtures at the advertised page size Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index b8444167ba..fd254908f9 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -176,7 +176,7 @@ def pages(name, path): page_digests[name] = digest # Plan only read offsets. Every actual model-facing page must # come from the public CLI, including digest verification. - page_size = 4096 if adaptive or host == "code_mode" else 5000 + page_size = 3072 if adaptive or host == "code_mode" else 5000 offsets, start, count = [0], 0, 0 for character in raw.decode(): size = len(character.encode()) From a9a67f3c8ad9fc8cbde622630e71c5c9f4dfe573 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:46:58 +0800 Subject: [PATCH 04/40] test(context): compare complete wires after paged presentation Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index fd254908f9..f1b603ac6e 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -485,7 +485,9 @@ def held(*args, **kwargs): captured = (list((workspace / "wakes").glob("*/decisions/*/decision.json")) if host == "benchmark" else list(root.glob("*/decision.json"))) assert len(captured) == 2 - assert wire_paths["warm"].stat().st_size < wire_paths["cold"].stat().st_size + # Compare complete presentation bytes, not two similarly sized first-page descriptors. + # Whole CLI/model costs remain a separate benchmark measurement. + assert len(json.dumps(wires["warm"]).encode()) < len(json.dumps(wires["cold"]).encode()) assert len(calls) == len(observed) and stages["baseline_confirmed"] if native_read_batch > 2: visible_batches = [] From 31d56b2a000f8efae09be2ff908ce5ef444bab73 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:06:02 +0800 Subject: [PATCH 05/40] fix(context): accept native null default provider Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/turn_driver/codex_context.py | 5 +++-- tests/control_plane/test_codex_context_lifecycle.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 146b73b169..d8b280841a 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -227,10 +227,11 @@ def codex_context_transport_configuration(native_config: Mapping[str, Any]) -> d explicit operator providers are left alone. This config grants no authority and does not assert that any particular input was complete. """ - if native_config.get("model_provider", "openai") != "openai": + # Native config/read serializes an unset default as null. + if native_config.get("model_provider") not in (None, "openai"): return {} provider = "loopx_context_http" - if provider in native_config.get("model_providers", {}): + if provider in (native_config.get("model_providers") or {}): raise ValueError("context HTTP provider conflicts with existing native configuration") return {"model_provider": provider, "model_providers": {provider: { "name": "OpenAI", "wire_api": "responses", "requires_openai_auth": True, diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 6c31a5778e..81cf6fc959 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -73,12 +73,14 @@ def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_p assert missing_identity.returncode == 1 and not json.loads(missing_identity.stdout)["ok"] -def test_context_transport_retains_native_auth_and_explicit_provider_ownership(): +@pytest.mark.parametrize("native_config", [{}, {"model_provider": None, "model_providers": None}, + {"model_provider": "openai"}]) +def test_context_transport_retains_native_auth_and_explicit_provider_ownership(native_config): original = {"model_provider": "operator", "model_providers": {"operator": { "base_url": "https://example.invalid", "supports_websockets": True}}} assert context.codex_context_transport_configuration(original) == {} assert original["model_providers"]["operator"]["supports_websockets"] is True - native = context.codex_context_transport_configuration({"model_provider": "openai"}) + native = context.codex_context_transport_configuration(native_config) selected = native["model_provider"] assert native["model_providers"][selected] == { "name": "OpenAI", "wire_api": "responses", "requires_openai_auth": True, From f77dc729d712e83eda9ed1b24a58124f36b9d403 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:17:41 +0800 Subject: [PATCH 06/40] fix(context): bind streamed calls before live execution Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../turn_driver/codex_input_trace.py | 17 ++++++-- .../test_codex_input_retention.py | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/loopx/control_plane/turn_driver/codex_input_trace.py b/loopx/control_plane/turn_driver/codex_input_trace.py index aa489f2325..4ddb2e1967 100644 --- a/loopx/control_plane/turn_driver/codex_input_trace.py +++ b/loopx/control_plane/turn_driver/codex_input_trace.py @@ -87,7 +87,7 @@ def _native_tool_is_bound(tail: list[dict[str, Any]], completed: list[dict[str, and isinstance(tool.get("code_mode_runtime_tool_id"), str) and bool(tool["code_mode_runtime_tool_id"]) and tool.get("model_visible_call_id") is None and len(cells) == 1 - and completed[0]["seq"] < cells[0]["seq"] < tools[0]["seq"] + and tail[0]["seq"] < cells[0]["seq"] < tools[0]["seq"] and isinstance(cells[0]["payload"].get("source_js"), str) and isinstance(cells[0]["payload"].get("model_visible_call_id"), str) and not any(row["payload"].get("type") == "code_cell_ended" @@ -186,13 +186,24 @@ def codex_context_in_active_input(observation: dict[str, Any], *, baseline: dict and row["payload"].get("inference_call_id") == inference] tools = [row for row in tail if row["payload"].get("type") == "tool_call_started" and row["payload"].get("tool_call_id") == tool_id] - if len(completed) != 1 or len(tools) != 1 or not tail[0]["seq"] < completed[0]["seq"] < tools[0]["seq"]: + if (len(completed) != 1 or len(tools) != 1 + or not tail[0]["seq"] < min(completed[0]["seq"], tools[0]["seq"])): return False + if completed[0]["seq"] > tools[0]["seq"]: + # Streaming announces a tool/cell before response completion. The + # complete model output must still precede this exact live runtime; + # an announcement or an unfinished inference alone proves nothing. + runtimes = [row for row in tail if row["payload"].get("type") == "tool_call_runtime_started" + and row["payload"].get("tool_call_id") == tool_id] + if (len(runtimes) != 1 + or not completed[0]["seq"] < runtimes[0]["seq"]): + return False tool = tools[0]["payload"] if tool.get("kind") != {"type": "exec_command"}: return False if any(row.get("codex_turn_id") != turn or row["payload"].get("type") == "codex_turn_ended" - or (row["payload"].get("type") == "tool_call_ended" and row["payload"].get("tool_call_id") == tool_id) + or (row["payload"].get("type") in ("tool_call_ended", "tool_call_runtime_ended") + and row["payload"].get("tool_call_id") == tool_id) for row in tail): return False output = _read_bundle_payload(bundle, completed[0]["payload"]["response_payload"]) diff --git a/tests/control_plane/test_codex_input_retention.py b/tests/control_plane/test_codex_input_retention.py index 5ba5614e4d..4febe3f3f1 100644 --- a/tests/control_plane/test_codex_input_retention.py +++ b/tests/control_plane/test_codex_input_retention.py @@ -237,3 +237,44 @@ def test_code_mode_requires_exact_live_parent_and_complete_model_visible_output( (bundle / "payloads/input.json").write_text(json.dumps(request)) (bundle / "trace.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows)) assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") is (failure is None) + + +@pytest.mark.parametrize("code_mode", [False, True]) +@pytest.mark.parametrize("failure", [None, "no_runtime", "other_runtime", "duplicate_runtime", + "unfinished", "late_completion", "ended", "missing_bytes", "wrong_output"]) +def test_streamed_tool_needs_complete_response_before_its_live_runtime(tmp_path, code_mode, failure): + observation, baseline, request, original = fixture(tmp_path) + bundle = Path(observation["trace_bundle"]) + start, completion, tool = [row["payload"] for row in original] + payloads = [start] + if code_mode: + source = 'text(await tools.exec_command({cmd:"loopx quota should-run"}));' + payloads.append({"type": "code_cell_started", "runtime_cell_id": "cell", + "model_visible_call_id": "parent", "source_js": source}) + tool.update(requester={"type": "code_cell", "runtime_cell_id": "cell"}, + model_visible_call_id=None, code_mode_runtime_tool_id="child") + output = {"output_items": [{"type": "custom_tool_call", "name": "exec", "call_id": "parent", "input": source}]} + (bundle / "payloads/output.json").write_text(json.dumps(output)) + payloads.append(tool) + runtime = {"type": "tool_call_runtime_started", "tool_call_id": "guard"} + payloads += [completion, runtime] + if failure == "no_runtime": + payloads.remove(runtime) + elif failure == "other_runtime": + runtime["tool_call_id"] = "other" + elif failure == "duplicate_runtime": + payloads.append(dict(runtime)) + elif failure == "unfinished": + payloads.remove(completion) + elif failure == "late_completion": + payloads[-2:] = [runtime, completion] + elif failure == "ended": + payloads.append({"type": "tool_call_runtime_ended", "tool_call_id": "guard"}) + elif failure == "missing_bytes": + request["input"].pop(1) + elif failure == "wrong_output": + (bundle / "payloads/output.json").write_text(json.dumps({"output_items": []})) + (bundle / "payloads/input.json").write_text(json.dumps(request)) + rows = [{**original[0], "seq": i, "payload": payload} for i, payload in enumerate(payloads)] + (bundle / "trace.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows)) + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") is (failure is None) From 6bac693c4a56d2b55c3e59c50964c8a5b5a5d5c8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:17:41 +0800 Subject: [PATCH 07/40] docs(context): clarify reader and native digest verification Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 16 +++++++++++----- .../turn_driver/decision_capture.py | 8 +++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 8ced7f6fcf..e461c3bf17 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -215,6 +215,9 @@ recovery without another admission. Native pages stay at 3 KiB throughout that read, reducing output-wrapper truncation; consume the inline page before following next_command. Forward exec stdout directly in code mode. Repeating the packet in a hashing tool call does not establish that its original bytes were retained. +The reader verifies the immutable capture digest; the native observer verifies +complete current-input bytes before reuse. With streaming tools, the final +model output must bind the announced call before its live runtime starts. 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 @@ -340,6 +343,8 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 固定摘要的后续读取与完整恢复,不另行准入。该原生读取链的页大小保持 3 KiB,以减少 输出包装造成的截断;先消费已附带页,再跟随 next_command。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 +读取器校验不可变 capture 的摘要;原生观察器在复用前校验当前输入中的完整字节。 +流式工具可先公布调用,但完整模型输出必须在该调用的实际 runtime 启动前完成绑定。 带 `previous_response_id` 的原生隐式历史请求不能给当前观察器提供完整保留证明。 默认 OpenAI 配置模板因此使用进程内的原生 HTTP provider,保留 Codex 登录和路由, @@ -371,9 +376,9 @@ 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 -to leave room for projection metadata; subsequent pages retain the normal 4 KiB -size. Consume `content`, then follow -`next_command` through `eof`; verify full byte coverage and the digest. The +to leave room for projection metadata; subsequent native pages also use 3 KiB. +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 output. If the optional first-page read fails, that same command remains usable. Off responses do not add a page. No new flag, admission or retention receipt is @@ -387,8 +392,9 @@ introduced; an inline page alone cannot prove complete input retention. 原始摘要和 Turn 回执,不重新准入或扣减额度。 开启时,成功的已保存投影同时用现有 `context_capture_page_v1` 字段返回第一页。 -该页内容最多 3 KiB,为投影元数据留出输出空间;后续仍按正常 4 KiB 读取。 -先读取 `content`,再沿 `next_command` 读至 `eof`,核验完整字节覆盖与摘要。 +该页内容最多 3 KiB,为投影元数据留出输出空间;后续原生页也按 3 KiB 读取。 +先读取 `content`,再沿 `next_command` 读至 `eof`,确保完整字节覆盖。 +读取器校验原始摘要;无需重抄正文或计算哈希。 原有 `command` 仍从零开始,供旧读取方或内联输出不完整时重读;可选的首屏读取 失败时也保留该命令。关闭态不增加分页数据,不新增开关、准入或保留回执; 单页内联内容不证明完整输入仍被保留。 diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 74ffbcb082..26cc1230f5 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -103,11 +103,13 @@ def validate_handoff_socket(value: str) -> Path: CONTEXT_CAPTURE_READ_GUIDANCE = ( "Follow next_command through eof and combine content in byte-offset order. " "In code-mode, forward successful exec stdout directly with text(result.output); " - "retain errors. Do not retype the packet into a hashing command; that is not current-input retention proof. " + "retain errors. The reader verifies the original capture SHA-256; keep its returned digest and byte cursors. " + "Do not hash or retype the packet. Native input observation independently verifies complete model-visible " + "bytes before delta reuse; neither a retyped hash nor a disk hash proves that input. " "Small batches can save model round trips; start with at most two default pages " "and inspect every output. A host may truncate inside valid JSON or omit a whole output. " - "Check each model-visible content's UTF-8 byte length against next_offset (total_bytes at eof), " - "complete coverage and the original SHA-256. If incomplete, reread the affected range " + "Consume all model-visible content in UTF-8 byte-offset order with complete coverage through total_bytes. " + "If a range or output is missing, truncated or uncertain, reread the affected range " "of this same capture with fewer pages or a smaller --page-bytes; do not trust a preview or EOF alone. " "Do not rerun admission or ask the owner. File existence and page EOF are not retention proof." ) From a12a077382872487bb4f193eb221b1c91bdedcc1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:25:55 +0800 Subject: [PATCH 08/40] refactor(context): use explicit native lifecycle continuity assumptions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/runtime/context_delivery.py | 35 +-- benchmark/runtime/worker.py | 11 +- benchmark/tests/test_shared_codex_runtime.py | 21 +- docs/heartbeat-automation-prompt.md | 39 +-- docs/reference/required-work-context.md | 85 ++++--- .../machine_configuration/context_delivery.py | 3 +- loopx/cli_commands/context_machine.py | 12 + loopx/cli_commands/quota.py | 4 +- loopx/cli_commands/quota_capture.py | 30 ++- loopx/control_plane/quota/context_delivery.ts | 24 +- .../quota/native_context_lifecycle.ts | 107 ++++++++ loopx/control_plane/quota/turn_envelope.py | 15 +- loopx/control_plane/quota/turn_envelope.ts | 2 + .../turn_driver/codex_context.py | 216 ++++++++-------- .../turn_driver/decision_capture.py | 8 +- .../test_codex_context_capture.py | 7 +- .../test_codex_context_lifecycle.py | 232 ++++++++++-------- .../test_context_machine_configuration.py | 34 ++- .../test_saved_context_project.py | 7 +- 19 files changed, 552 insertions(+), 340 deletions(-) create mode 100644 loopx/control_plane/quota/native_context_lifecycle.ts diff --git a/benchmark/runtime/context_delivery.py b/benchmark/runtime/context_delivery.py index f05e2a4981..cae1795b81 100644 --- a/benchmark/runtime/context_delivery.py +++ b/benchmark/runtime/context_delivery.py @@ -10,14 +10,13 @@ import stat import shlex from pathlib import Path -import tempfile def preflight_context_instruction(env: dict[str, str], path: Path, digest: str, turn_id: str) -> str: """Delay presentation of the original preflight until its native tool call. - No model input exists at host preflight time. Keep the original source/hash - and let the product's current-input observer choose full/delta later. + Keep the original source/hash. The product's lifecycle owner chooses the + full/delta view when the native tool actually presents it. """ from loopx.command_invocation import loopx_cli_argv @@ -31,7 +30,8 @@ def preflight_context_instruction(env: dict[str, str], path: Path, digest: str, + "\n```\nConsume any returned page content, then follow next_command through eof. " "If no page content is returned, run command and follow all next_command pages. Reconstruct any delta against " "its exact retained baseline before following the original interaction_contract. " - "If projection is unavailable, the baseline is absent/uncertain, or a response is incomplete, " + "Normal completed reads use the explicit native-hook continuity assumption. " + "If projection is unavailable, the baseline was invalidated, or a response is incomplete, " "follow its full recovery reference. If the projection command itself fails, read the original " "complete packet with:\n```sh\n" + shlex.join(full) + "\n```\nFollow every next_command. " "Neither presentation nor recovery renews this decision's authority or freshness. ") @@ -39,23 +39,15 @@ def preflight_context_instruction(env: dict[str, str], path: Path, digest: str, def prepare_context_delivery(env: dict[str, str], home: Path) -> None: from loopx.control_plane.turn_driver.codex_context import ( - codex_context_configuration, codex_context_transport_configuration, + CONTEXT_LIFECYCLE_HOOKS, codex_context_configuration, ) from loopx.control_plane.turn_driver.codex_stdio import StdioNativeGoalTransport root = home / "loopx-context" - traces = home / "context-traces" - for directory in (root, traces): - directory.mkdir(mode=0o700, exist_ok=True) - info = directory.lstat() - if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: - raise ValueError("context diagnostics require private, physical trial directories") - # Opt-in diagnostics are private to this trial, never global or a retention - # assertion. Include all IO/model-input costs when comparing this treatment. - # Keep candidate references across resumed wakes, but inspect only this - # native invocation's diagnostics. Older traces are evidence, not a growing - # hot-path scan or a reason to distrust intact current model input. - env["CODEX_ROLLOUT_TRACE_ROOT"] = tempfile.mkdtemp(prefix="wake-", dir=traces) + root.mkdir(mode=0o700, exist_ok=True) + info = root.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise ValueError("context delivery requires a private, physical trial directory") env["LOOPX_CODEX_CONTEXT_DIR"] = str(root) configuration = codex_context_configuration(str(root), goal_id=env["LOOPX_GOAL_ID"], agent_id=env["LOOPX_AGENT_ID"], scenario="codex_benchmark_heartbeat", cli_argv=[env["LOOPX_CLI"], "--cli-bin", env["LOOPX_CLI"]]) @@ -81,12 +73,10 @@ def prepare_context_delivery(env: dict[str, str], home: Path) -> None: native.request("initialize", {"clientInfo": {"name": "loopx_context_transport", "version": "0.1.0"}, "capabilities": {"experimentalApi": True}}) native.notify("initialized", {}) - effective = native.request("config/read", {"cwd": env["LOOPX_PROJECT"], "includeLayers": False}) - transport = codex_context_transport_configuration(effective["config"]) listed = native.request("hooks/list", {"cwds": [env["LOOPX_PROJECT"]]}) hooks = [hook for entry in listed["data"] for hook in entry["hooks"] if hook.get("sourcePath") == str(path.resolve())] - if len(hooks) != 2 or path.read_text() != content: + if len(hooks) != len(CONTEXT_LIFECYCLE_HOOKS) or path.read_text() != content: raise ValueError("native host did not discover the exact trial context hooks") entries = [] for hook in hooks: @@ -100,9 +90,4 @@ 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"])] - if transport: - provider = transport["model_provider"] - native_args += ["-c", "model_provider=" + json.dumps(provider)] - for key, value in transport["model_providers"][provider].items(): - native_args += ["-c", f"model_providers.{provider}.{key}=" + json.dumps(value)] env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"] = json.dumps(native_args) diff --git a/benchmark/runtime/worker.py b/benchmark/runtime/worker.py index fc24648b02..8425ab97fe 100644 --- a/benchmark/runtime/worker.py +++ b/benchmark/runtime/worker.py @@ -514,19 +514,16 @@ def run_once(env: dict[str, str]) -> dict: if execution.context_delivery != "off": from benchmark.runtime.context_delivery import prepare_context_delivery - prior_trace = env.get("CODEX_ROLLOUT_TRACE_ROOT") try: - prepare_context_delivery(env, home) + context_env = dict(env) + prepare_context_delivery(context_env, home) + env.update(context_env) receipt["context_transport"] = {"available": True} except Exception as exc: # Optional presentation cannot block an otherwise admitted - # trial. Unsupported hooks/diagnostics keep ordinary full IO. + # trial. Unavailable native hooks keep ordinary full IO. env.pop("LOOPX_CODEX_CONTEXT_DIR", None) env.pop("LOOPX_CODEX_CONTEXT_TRUST_ARGS", None) - if prior_trace is None: - env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) - else: - env["CODEX_ROLLOUT_TRACE_ROOT"] = prior_trace receipt["context_transport"] = {"available": False, **_error_tail(exc)} body = "Finish the task." if stage == "plan": diff --git a/benchmark/tests/test_shared_codex_runtime.py b/benchmark/tests/test_shared_codex_runtime.py index 7a5b4f335c..cba544c5e8 100644 --- a/benchmark/tests/test_shared_codex_runtime.py +++ b/benchmark/tests/test_shared_codex_runtime.py @@ -1151,7 +1151,7 @@ def test_context_setup_preserves_unsafe_or_unowned_inputs(tmp_path, kind): protected = home / "hooks.json" protected.write_text('{"operator": "unchanged"}') else: - protected = home / "context-traces" + protected = home / "loopx-context" if kind == "public_directory": protected.mkdir(mode=0o755) protected.chmod(0o755) @@ -1175,7 +1175,8 @@ def test_context_discovery_keeps_solver_configuration_and_unrelated_hooks(tmp_pa home = tmp_path / "home" home.mkdir() config = home / "config.toml" - original_config = b"[features]\nplugins=true\nremote_plugin=true\nplugin_sharing=true\n" + original_config = (f'model_provider={json.dumps(provider)}\n' + + '[features]\nplugins=true\nremote_plugin=true\nplugin_sharing=true\n').encode() config.write_bytes(original_config) methods, commands = [], [] @@ -1186,11 +1187,9 @@ def request(method, params): methods.append(method) if method == "initialize": return {} - if method == "config/read": - return {"config": {"model_provider": provider}} assert method == "hooks/list", "discovery must not start model work" own = [{"sourcePath": str((home / "hooks.json").resolve()), - "key": f"own-{i}", "currentHash": f"native-hash-{i}"} for i in range(2)] + "key": f"own-{i}", "currentHash": f"native-hash-{i}"} for i in range(5)] return {"data": [{"hooks": [*own, {"sourcePath": str(tmp_path / "other-hooks.json"), "key": "unrelated", "currentHash": "never-trust-this"}]}]} yield SimpleNamespace(request=request, notify=lambda *args: None) @@ -1199,7 +1198,7 @@ def request(method, params): env = {"LOOPX_CLI": "fixture-loopx", "LOOPX_GOAL_ID": "fixture", "LOOPX_AGENT_ID": "agent", "CODEX_BIN": "native-codex", "LOOPX_PROJECT": str(tmp_path)} prepare_context_delivery(env, home) - assert methods == ["initialize", "config/read", "hooks/list"] + assert methods == ["initialize", "hooks/list"] assignments = {commands[0][i + 1] for i, value in enumerate(commands[0]) if value == "-c"} assert assignments == {"features.plugins=false", "features.remote_plugin=false", "features.plugin_sharing=false"} trusted = json.loads(env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"]) @@ -1210,13 +1209,9 @@ def request(method, params): assert config.read_bytes() == original_config overrides = {trusted[i + 1].split("=", 1)[0]: trusted[i + 1].split("=", 1)[1] for i, value in enumerate(trusted) if value == "-c"} - if provider == "openai": - selected = json.loads(overrides["model_provider"]) - assert json.loads(overrides[f"model_providers.{selected}.supports_websockets"]) is False - assert json.loads(overrides[f"model_providers.{selected}.requires_openai_auth"]) is True - assert not any(key.endswith(("base_url", "env_key")) for key in overrides) - else: - assert not any(key.startswith("model_provider") for key in overrides) + assert not any(key.startswith("model_provider") for key in overrides) + assert "CODEX_ROLLOUT_TRACE_ROOT" not in env + assert not (home / "context-traces").exists() @pytest.mark.parametrize("value", [True, False, "1", "codex_benchmark_heartbeat", "codex_app_heartbeat", "all"]) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index c871cd3ee3..571fb745e6 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -438,7 +438,7 @@ is only one field. With `on`, missing, incomplete, contradictory or unsupported run-mode declarations are configuration errors: the CLI exits nonzero with a reason before admission or transport writes, including in native hooks. Correct the declaration or select `off`; there is no owner question or new approval. -In a supported run mode, missing or invalid retained-input evidence still +In a supported run mode, a missing or invalid continuity basis still delivers the same original full capture. The current recovery reference is saved before optional delta computation, so an oversized warm request or failed candidate update cannot select an older capture after compaction or resume. @@ -449,8 +449,9 @@ can be established, or invalidation also fails, the CLI reports rerun admission to recover a presentation failure. `off` creates no native journal, hook configuration or input observation; directories alone never turn it on. This declaration check is not -host authentication, execution authority or proof of retained input. Every warm -delta still needs independently verified complete current-input evidence. +host authentication, execution authority or proof of retained input. Native +delivery uses the explicit lifecycle assumption described below; final-input +Host clients keep their independent retained-input confirmation contract. Generic CLI, visible native Goals, Claude and other hosts remain outside this opt-in. Harbor/container and installed desktop adoption retain their separate unqualified boundaries; listing an entry does not certify live adoption. @@ -480,20 +481,28 @@ these options without freezing a Turn ID. Each wake loads current instructions and binds the existing `LOOPX_TURN` before its generated guard. Selection keeps the same transport. An unset wake identity fails in the shell before admission. -Warm delta additionally needs the native host process to have -`CODEX_ROLLOUT_TRACE_ROOT` set to private native trace-v1 diagnostics before it -starts. A hook cannot set its parent process environment. If that host cannot -provide the required actual-input observation, ordinary full delivery continues; -do not use a file, session ID or successful setup as a retention acknowledgement. -Scope diagnostics to the authorized host and include their IO/retention cost. +Native reuse assumes a normally completed full read was read and remains +available until a lifecycle invalidation. `PostToolUse` merges complete reader +ranges in the existing private session journal. EOF alone, known read failures, +malformed/truncated pages and interrupted paging cannot establish a baseline. +Compaction, clear, fork, session/workspace change and an unresumable restore +invalidate it; ordinary tools and turn endings do not. Same-session resume can +reuse a completed baseline. Repeated events and parallel pages are idempotent. +This replaces per-request input-history proof, not the capture integrity check. +It preserves native providers, authentication, endpoints and connection mode, +including implicit history. No input diagnostics or forced HTTP is required. +Unreported downstream output loss or missed/externally disabled hooks can escape +detection; this is an engineering assumption, not measured retention or a model ACK. Read back native hook discovery/trust and the generated guard options, then -verify cold full, warm delta only after complete current-input evidence, +verify cold full, warm delta after completed reader delivery, resume and compaction recovery on the actual host. The persistent instruction names `quota context-read --codex-context-root ...` when an entire hook is lost; lookup is read-only and subsequent pages pin the original file/hash. It never readmits or spends. Normal native trust/settings UI remains the activation and -error surface; unsupported diagnostics keep the same full path. +error surface; failed setup keeps the full path. Explicit off stops projection. +Replacing machine configuration also invalidates reuse, so an off/on round trip +cannot reuse a baseline that missed lifecycle events while disabled. An existing decision-handoff socket may use this same capture. Handoff validates and releases the immutable original first; native projection follows release. It does not create a second checkpoint or change the handoff's failure/recovery @@ -511,8 +520,8 @@ loopx --format json quota context-project --context-delivery on --codex-app \ The JSON response supplies `mode`, the digest-pinned read `command`, the target when available, and an original full `recovery` reference. Cold mode delivers -complete context plus its target; a warm delta still requires actual current-input -proof. Follow every page and reconstruct before dependent work. Optional codec +complete context plus its target; a warm delta names its native-hook continuity +basis. Follow every page and reconstruct before dependent work. Optional codec or delivery-cache failure after recovery publication falls back to that same full capture. Enabled native preparation or unsafe recovery publication fails with `ok: false`, `quota_decision_transport_failed`, and the original `recovery` @@ -523,8 +532,8 @@ admission or accounting. The original file stays unchanged; generated caches remain private. A missing projection command can use `quota context-read` with the original file/hash. Set `--context-delivery off` and remove context options from generated host configuration to restore ordinary full-read instructions. Remove only these generated hook entries and -appended recovery instruction from native configuration, and disable their -owned diagnostics if enabled. Preserve unrelated hooks/settings; the CLI does +appended recovery instruction from native configuration. Preserve unrelated +diagnostics, providers, hooks and settings; the CLI does not edit host configuration. No stored decision or receipt is rewritten. To roll back, remove the context/capture options from this bootstrap and remove only the generated hooks/recovery instruction from the native configuration. diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index e461c3bf17..e44ba93235 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -140,7 +140,7 @@ transport flag does not restore the old exact Todo schema. ## Opt-in native reuse The existing [native context delivery option](../heartbeat-automation-prompt.md#opt-in-native-context-delivery) -uses one TypeScript codec for ordinary quota and TurnEnvelope. Within a confirmed +uses one TypeScript codec for ordinary quota and TurnEnvelope. Within a usable baseline, both representations repeat `planning_horizon.core_goal`'s original objective, provenance and availability, including a changed or missing objective. The complete `acceptance_contract` and registered planning trees use exact @@ -163,7 +163,7 @@ codec compares this encoding with ordinary edits and full replacement before choosing. A reader that does not recognize the edit recovers the full capture. These are presentation rules within the existing default-off option, not a new -configuration or admission policy. Missing retention, a changed epoch, a damaged +configuration or admission policy. A missing continuity basis, a changed epoch, a damaged patch or an incompatible reader automatically recovers the same original full capture. Recovery never reruns the guard, asks the owner, spends quota or grants execution. Wire reduction alone does not establish actual model costs or outcomes. @@ -215,28 +215,42 @@ recovery without another admission. Native pages stay at 3 KiB throughout that read, reducing output-wrapper truncation; consume the inline page before following next_command. Forward exec stdout directly in code mode. Repeating the packet in a hashing tool call does not establish that its original bytes were retained. -The reader verifies the immutable capture digest; the native observer verifies -complete current-input bytes before reuse. With streaming tools, the final -model output must bind the announced call before its live runtime starts. +The reader verifies the immutable capture digest. Native lifecycle delivery uses +an explicit engineering assumption: a normally completed full read was read and +remains available until invalidated. This is not a retained-input observation or +a model ACK. The shared codec still validates the exact baseline digest, scope, +current controls and reconstruction. Explicit final-input Host clients retain +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. It does not install or trust hooks, -change host configuration or enable diagnostics in a running parent. The native -process still needs its exact reviewed hooks and `CODEX_ROLLOUT_TRACE_ROOT` -configured before startup. Without current-input retention proof, delivery stays -full. Native implicit-history requests with `previous_response_id` are not retention -proof for this observer. The rendered default OpenAI configuration therefore uses -a native HTTP provider, keeping Codex authentication and routing -without an endpoint or credential override. Benchmark preparation reads effective -native configuration before applying it; explicit operator providers are preserved, -and a conflicting provider id is rejected. A manual App setup must also preserve -an explicit provider and verify its actual input. HTTP transport and diagnostics -have costs which remain part of the comparison; selecting them does not prove a -delta or a performance gain. Benchmark overrides are per native invocation; a manual -App setup restores its previous provider when removing this configuration. +change host configuration or restart the running App. Configure and trust its +exact hooks for the next native session. The native path preserves authentication, +endpoints, provider selection and connection mode, including implicit history via +`previous_response_id`. It neither enables input diagnostics nor scans model-input +history. Benchmark preparation stages those same hooks in its isolated trial home. + +`PostToolUse` records complete digest-bound reader ranges in the existing session +journal; EOF without preceding pages, a known malformed/truncated page or a reader +error cannot establish a baseline. Concurrent and repeated pages coalesce. +`PreCompact`, `PostCompact`, clear, fork, a changed session/workspace and an +unresumable restore invalidate it. Same-session resume preserves a complete +baseline; ordinary tools and turn endings do not invalidate it. An interrupted +pending delivery requires full recovery. Repeated reset events are idempotent. +Recovery reads the same immutable capture without re-admission or spend. + +This deliberately replaces per-request complete-input proof with lifecycle +assumptions. Downstream output loss or truncation not exposed to the hook, a +missed invalidation, or hooks silently disabled outside the declared setting may +escape detection. These limits are not certified retention and do not add an +approval gate. Explicit off keeps ordinary full delivery; known journal failures +remove the reusable baseline and retain the original explicit recovery reference. Generated machine-inherited hooks reload the same machine runtime, so off or removal stops their journal writes. Explicit manual on options remain explicit. +Replacing the machine configuration invalidates reuse, including off/on round +trips with identical final contents. Unrelated configuration edits may therefore +cause one conservative full delivery. Generate a persistent entrypoint with `heartbeat-prompt --bootstrap`. It retains caller-supplied options, including an explicit off, but never freezes resolved @@ -293,7 +307,7 @@ Goal adapter 仍完整读取文件、校验 UTF-8,并以全部字节计算修 也不证明模型已采用或获得质量收益。回滚需恢复旧版本并重新生成提示。 既有默认关闭的原生 context delivery 由同一 TypeScript codec 处理普通 quota 与 -TurnEnvelope。确认基线保留后,两入口仍逐轮发送 `planning_horizon.core_goal` 的 +TurnEnvelope。在基线连续性成立的口径内,两入口仍逐轮发送 `planning_horizon.core_goal` 的 原始目标、来源和可用性,包括目标改变或缺失。完整 `acceptance_contract` 与已登记 规划树按精确复用、子树变化或全量交付;累计验收树不等于原始目标句。未知 horizon 字段仍作为本轮输入。quota 通道与 Turn 契约摘要复用完整静态收口指导,当前收口判断、 @@ -304,7 +318,7 @@ TurnEnvelope。确认基线保留后,两入口仍逐轮发送 `planning_horizo `removed`;数组的 `items` 与 `values` 使用精确索引,`length` 调整长度。 字段仍在本轮输入中,只减少重复 wrapper;codec 比较普通编辑、合并字段 与完整替换后选择,无法识别该编辑的读者恢复完整 capture。 -这些只改变既有选项内的展示,不新增配置或准入。保留不明、epoch 改变、patch 损坏 +这些只改变既有选项内的展示,不新增配置或准入。缺少可用连续性依据、epoch 改变、patch 损坏 或读者不兼容时,自动恢复同一原始 full capture,不重跑 guard、不问询、不扣额, 也不授予执行权限。传输减少本身不证明实际模型费用或结果改善。 @@ -343,18 +357,27 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 固定摘要的后续读取与完整恢复,不另行准入。该原生读取链的页大小保持 3 KiB,以减少 输出包装造成的截断;先消费已附带页,再跟随 next_command。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 -读取器校验不可变 capture 的摘要;原生观察器在复用前校验当前输入中的完整字节。 -流式工具可先公布调用,但完整模型输出必须在该调用的实际 runtime 启动前完成绑定。 - -带 `previous_response_id` 的原生隐式历史请求不能给当前观察器提供完整保留证明。 -默认 OpenAI 配置模板因此使用进程内的原生 HTTP provider,保留 Codex 登录和路由, -不改端点或凭证。Benchmark 准备会读取实际原生配置;显式自定义 provider 保持原样, -名称冲突则拒绝。手工配置 App 也须保留显式 provider,并验证实际输入。HTTP 与诊断 -成本仍须计入比较,选择它们不代表已经产生差分或性能收益。Benchmark 覆盖只作用于 -本次原生调用;手工 App 配置撤回时也须恢复此前的 provider。 -原生进程启动前仍须配置已审阅的精确 hook 与 `CODEX_ROLLOUT_TRACE_ROOT`;无法证明当前 -输入保留时返回完整捕获。由机器继承生成的 hook 每次重读同一个 runtime,关闭或移除 +读取器校验不可变 capture 的摘要。原生路径明确采用工程假设:正常完整交付的首包已读, +在失效事件发生前仍可用;这不是实际输入保留证明或模型 ACK。共享 codec 仍校验精确基线 +摘要、作用域、本轮控制与完整重建。显式 final-input Host 客户端保留独立的确认输入契约。 + +为下次原生会话配置并信任精确 hooks,不重启当前 App。原生路径保留认证、端点、provider +和连接方式,包括带 `previous_response_id` 的隐式历史;不启用输入诊断,也不逐轮扫描 +模型完整输入。Benchmark 在各自隔离的 trial home 内自动准备同一组 hooks。 + +`PostToolUse` 在现有 session journal 中合并摘要固定的完整读取范围;缺少前页的 EOF、 +已知的格式损坏/截断或读取失败不能建立基线。并发、重复页幂等合并。`PreCompact`、 +`PostCompact`、清空、分叉、会话/工作区切换及无法接续的恢复使基线失效;同会话的完整 +基线可在 resume 后继续使用,普通工具和正常轮次结束不无条件失效。尚未完成的交付被中断 +后须完整恢复。重复重置事件幂等;恢复读取同一不可变捕获,不重新准入或扣额。 + +这是以生命周期假设替换逐轮完整输入证明。Hook 未暴露的下游丢失/截断、漏掉失效事件, +或在声明开关之外静默禁用 hooks,仍可能漏检;不把这些假设称为实测保留,也不新增审批门。 +显式关闭继续普通全量投递;已知 journal 失败移除可复用基线,保留原始显式恢复引用。 +由机器继承生成的 hook 每次重读同一个 runtime,关闭或移除 配置后停止 journal 写入;手工显式 on 保留其显式优先级。 +机器配置被替换后重新全量,包括最终内容相同的 off/on 往返;其他机器配置编辑也可能 +触发一次保守的全量投递。 通过 `heartbeat-prompt --bootstrap` 生成持久入口,仅保留调用者显式参数(包括 off), 不固化当前机器默认值或自动生成路径。已有 v2 loader 每次唤醒重读设置;旧展开式提示须 diff --git a/loopx/capabilities/machine_configuration/context_delivery.py b/loopx/capabilities/machine_configuration/context_delivery.py index acd9db75d5..b1d41d99ad 100644 --- a/loopx/capabilities/machine_configuration/context_delivery.py +++ b/loopx/capabilities/machine_configuration/context_delivery.py @@ -29,7 +29,8 @@ def context_delivery_machine_configuration_namespace() -> MachineConfigurationNa title="Differential context", description=( "Reuse unchanged context for Codex App and benchmark heartbeats. Off by default; " - "unsupported declarations fail and unproven continuity keeps full context. " + "unsupported declarations fail and known context loss keeps full context. " + "Completed reads use an explicit native-hook continuity assumption. " "A saved preference does not establish host readiness or actual delta use." ), documentation={"path": "docs/reference/required-work-context.md#machine-default"}, diff --git a/loopx/cli_commands/context_machine.py b/loopx/cli_commands/context_machine.py index e0e47e3177..82352b46b5 100644 --- a/loopx/cli_commands/context_machine.py +++ b/loopx/cli_commands/context_machine.py @@ -73,6 +73,18 @@ def apply_context_machine_options( raise ValueError("native Codex context delivery needs JSON and a capture destination") if explicit is None: args._context_delivery_machine_runtime = str(runtime_root.expanduser().resolve()) + # An off/on round trip can miss lifecycle events. Store replacement + # invalidates reuse even when its content hash returns to the old value. + # This metadata only narrows reuse; it is not retention or work authority. + from ..capabilities.machine_configuration.store import machine_configuration_store_path + import hashlib + + try: + info = machine_configuration_store_path(runtime_root).stat() + generation = (info.st_dev, info.st_ino, info.st_mtime_ns, info.st_size) + args._context_delivery_generation = hashlib.sha256(repr(generation).encode()).hexdigest() + except FileNotFoundError: + args._context_delivery_generation = None location_missing = not (getattr(args, "codex_context_dir", None) or getattr(args, "codex_context_root", None) or getattr(args, "decision_handoff_socket", None)) capture_missing = (heartbeat or command == "should-run") and not ( diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index ac26634cb6..b55c95868d 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -538,11 +538,11 @@ def _emit_quota_result( payload = project_codex_context(payload, directory=directory, capture_directory=capture_directory, goal_id=decision_identity[0], agent_id=decision_identity[1], surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet", - capture_id=capture_id) + capture_id=capture_id, continuity_generation=getattr(args, "_context_delivery_generation", None)) from ..control_plane.turn_driver.decision_capture import CONTEXT_CAPTURE_READ_GUIDANCE recovery = payload.get("recovery") - view = native_context_capture_view(payload, capture_directory) + view = native_context_capture_view(payload, capture_directory, context_directory=directory) payload = {"ok": True, **view, "capture_sha256": capture_id, **({"recovery": recovery} if recovery else {}), "instruction": "Presentation of this original decision only; authority and freshness are unchanged. " diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index ae39eb2a63..6ddedb0496 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -104,7 +104,8 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: "recovery": "Restore the original digest-bound capture; no guard, admission or spend was performed."} -def native_context_capture_view(projected: dict[str, Any], directory: Path) -> dict[str, Any]: +def native_context_capture_view(projected: dict[str, Any], directory: Path, *, + context_directory: str | 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 @@ -126,14 +127,18 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path) -> d capture_sha256=reference["sha256"], offset=0, page_bytes=3072)) if page["ok"]: result.update(page) + from ..control_plane.turn_driver.codex_context import register_native_context_view + + register_native_context_view(context_directory, + {**result, "ancestry": projected.get("ancestry", 0)}) return result def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: """Present an immutable already-decided quota capture, without authority IO. - Publication is delayed until this native tool runs, so the same actual-input - observer can qualify a resumed baseline. Optional codec failure returns the + Publication is delayed until this native tool runs, so the lifecycle owner + can select its current continuity basis. Optional codec failure returns the original full read reference. An unsafe recovery selector fails explicitly with that same reference, never a replacement guard. Invalid run-mode opt-ins fail before that optional transport path. @@ -185,9 +190,10 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: cache = Path(tempfile.mkdtemp(prefix="context-", dir=root)) projected = project_codex_context(payload, directory=directory, capture_directory=cache, goal_id=args.goal_id, agent_id=args.agent_id, surface="quota_packet", - capture_id=hashlib.sha256(raw).hexdigest()) + capture_id=hashlib.sha256(raw).hexdigest(), + continuity_generation=getattr(args, "_context_delivery_generation", None)) native_prepared = True - native_view = native_context_capture_view(projected, cache) + native_view = native_context_capture_view(projected, cache, context_directory=directory) 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: @@ -426,16 +432,14 @@ 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": "For the default OpenAI provider, merge the rendered native HTTP provider to make complete inputs observable; " - "preserve an explicit operator provider and verify its actual input separately. " - "Merge hooks and append developer_instructions to the existing native configuration; " + "activation": "Merge hooks and append developer_instructions to the existing native configuration; " "review and trust only these exact hooks in Codex. Restart/resume with the resulting configuration. " + ("Machine defaults are re-read on each invocation; generated hooks inherit the same machine runtime. " "Omit per-command activation and location flags to retain that inheritance. " if machine_runtime else "Pass --context-delivery on with the same declared host mode, context location and a decision-output-root to heartbeat-prompt/should-run. ") + - "For delta, the native process must already have CODEX_ROLLOUT_TRACE_ROOT pointing to private diagnostics; " - "unavailable diagnostics preserve full delivery. Set --context-delivery off and remove context options to restore ordinary output. " + "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. " "This command installs nothing and grants no execution authority."} except (OSError, ValueError, TypeError) as exc: payload = {"ok": False, "reason": str(exc)} @@ -444,7 +448,9 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca if args.quota_command == "context-hook": import sys from ..control_plane.effect_runtime import MAX_REQUEST_BYTES - from ..control_plane.turn_driver.codex_context import codex_context_directory, codex_context_hook + from ..control_plane.turn_driver.codex_context import ( + CONTEXT_LIFECYCLE_HOOKS, codex_context_directory, codex_context_hook, + ) # Native transport only: no registry discovery, guard or quota effects. try: @@ -454,7 +460,7 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca raw = sys.stdin.buffer.read(MAX_REQUEST_BYTES + 1) event = json.loads(raw) if len(raw) <= MAX_REQUEST_BYTES else None if (isinstance(event, dict) and isinstance(event.get("session_id"), str) and event["session_id"] - and event.get("hook_event_name") in ("SessionStart", "PreCompact", "PreToolUse")): + and event.get("hook_event_name") in CONTEXT_LIFECYCLE_HOOKS): directory = codex_context_directory(directory=args.codex_context_dir, root=getattr(args, "codex_context_root", None), session=event.get("session_id"), create=True) payload = codex_context_hook(directory, event) diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 4cec6d33ea..8d976f5125 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -1,6 +1,6 @@ /** Presentation of immutable quota, Turn and work-context captures. No admission or state writes. - * A candidate delta is usable only at a host's final model-input assembly boundary; - * a previous CLI output or a successful capture handoff is not retention proof. */ + * Confirmed final input and explicit native-hook continuity assumptions remain + * distinct; successful capture handoff alone is never retained-input proof. */ import type {JsonObject} from "../effect_program.ts"; import {jsonObject, requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; import {canonicalAuthorityJson, canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; @@ -356,13 +356,15 @@ function targetBinding(request: JsonObject, context: JsonObject, surface: Surfac for (const key of ["goal_id", "agent_id", "session_id", "context_epoch"]) { binding[key] = requireNonEmptyString(scope[key], `scope.${key}`); } + if (scope.continuity_generation !== undefined) + binding.continuity_generation = requireNonEmptyString(scope.continuity_generation, "scope.continuity_generation"); binding.capture_id = requireNonEmptyString(request.capture_id, "capture_id"); binding.context_digest = digest(context); return binding; } function compatible(base: JsonObject, target: JsonObject): boolean { - return ["goal_id", "agent_id", "session_id", "context_epoch", "projection_version", "surface"] + return ["goal_id", "agent_id", "session_id", "context_epoch", "projection_version", "surface", "continuity_generation"] .every(key => base[key] === target[key]); } function surfaceOf(value: unknown): Surface { @@ -370,17 +372,20 @@ function surfaceOf(value: unknown): Surface { return value as Surface; } -/** Final-input host boundary only. Unsupported/uncertain retention returns full; - * the host ACKs intact delivery, never file creation or an Agent assertion. */ +/** Hosts may confirm final-input retention, or explicitly select the native + * hook delivery assumption. These bases are distinct; neither grants authority. */ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: readonly string[] = []): JsonObject { const context = requireJsonObject(request.context, "current projection"); const surface = surfaceOf(request.surface ?? "work_context"), target = targetBinding(request, context, surface); const full = (reason: string): JsonObject => ({schema_version: VERSION, mode: "full", target, ancestry: 0, reason, context: clone(context)}); const baseline = jsonObject(request.baseline), retained = jsonObject(request.retained); + const continuity = jsonObject(request.continuity); const binding = jsonObject(baseline?.target), prior = jsonObject(baseline?.context); - if (!binding || !prior || !retained || retained.complete !== true - || !equal(retained.target, binding) || !compatible(binding, target) + const confirmed = !continuity && retained?.complete === true && equal(retained.target, binding); + const assumed = !retained && continuity?.basis === "native_hook_assumption" + && equal(continuity.target, binding); + if (!binding || !prior || (!confirmed && !assumed) || !compatible(binding, target) || binding.context_digest !== digest(prior)) return full("baseline_unavailable"); const depth = baseline?.ancestry; if (!Number.isSafeInteger(depth) || Number(depth) < 0 || Number(depth) >= MAX_ANCESTRY) return full("full_refresh_due"); @@ -389,7 +394,10 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r const changes = changeTree(prior, context, after.paths); const delta: JsonObject = {schema_version: VERSION, mode: "delta", target, base: binding, ancestry: Number(depth) + 1, changes, - instruction: "Current controls are inline even when unchanged. Other fields inherit the confirmed baseline; all requirements remain binding. A value replaces its subtree; values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; + ...(assumed ? {continuity_basis: "native_hook_assumption"} : {}), + instruction: "Current controls are inline even when unchanged. Other fields inherit the " + + (assumed ? "exact baseline under the native-hook continuity assumption" : "confirmed baseline") + + "; all requirements remain binding. A value replaces its subtree; values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; if (bytes(delta) >= bytes(full("delta_not_smaller"))) return full("delta_not_smaller"); if (!equal(applyTree(prior, changes), context)) return full("reconstruction_failed"); return delta; diff --git a/loopx/control_plane/quota/native_context_lifecycle.ts b/loopx/control_plane/quota/native_context_lifecycle.ts new file mode 100644 index 0000000000..0558025cff --- /dev/null +++ b/loopx/control_plane/quota/native_context_lifecycle.ts @@ -0,0 +1,107 @@ +/** Native lifecycle assumptions for optional context transport. No work authority. + * Complete reader pages establish an explicit engineering assumption, not an + * observation that the host retained those bytes in a later model request. */ +import type {JsonObject} from "../effect_program.ts"; +import {jsonObject, requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; + +export const NATIVE_CONTEXT_BASIS = "native_hook_assumption"; + +function mergedRanges(value: unknown, start: number, end: number): number[][] { + const ranges = Array.isArray(value) ? value.filter((range): range is number[] => + Array.isArray(range) && range.length === 2 && range.every(Number.isSafeInteger)) : []; + ranges.push([start, end]); + ranges.sort((a, b) => a[0] - b[0]); + const merged: number[][] = []; + for (const range of ranges) { + const last = merged.at(-1); + if (last && range[0] <= last[1]) last[1] = Math.max(last[1], range[1]); + else merged.push([...range]); + } + return merged; +} + +/** Reduce the existing private native journal. Ordinary tools and turn endings + * preserve continuity. Failed/incomplete deliveries never create a baseline. */ +export function updateNativeContextLifecycle(request: JsonObject): JsonObject { + let state = structuredClone(requireJsonObject(request.state, "native context journal")); + const event = requireJsonObject(request.event, "native lifecycle event"); + // Native subagent hooks carry the parent session id. They cannot update the + // root journal; a child's own context must use its separate native session. + if (typeof event.agent_id === "string" && event.agent_id) return state; + const session = requireNonEmptyString(event.session_id, "native session id"); + const cwd = requireNonEmptyString(event.cwd, "native workspace"); + const same = state.session_id === session && state.cwd === cwd; + const name = event.hook_event_name; + if (!same) { + if (name !== "SessionStart") return state; + state = {session_id: session, cwd}; + } + const invalidate = () => { + // Reset is idempotent while already invalid. Pre/PostCompact and the + // subsequent SessionStart(compact) therefore do not rotate three epochs. + if (!state.context_epoch || state.candidate || state.pending) + state.context_epoch = requireNonEmptyString(request.new_epoch, "new native context epoch"); + delete state.candidate; + delete state.pending; + delete state.active_call; // retire legacy diagnostic-call binding + }; + if (name === "SessionStart") { + const resumable = same && event.source === "resume" && state.context_epoch + && !state.pending && jsonObject(state.candidate)?.basis === NATIVE_CONTEXT_BASIS; + if (!resumable) invalidate(); + return state; + } + if (name === "PreCompact" || name === "PostCompact") { + invalidate(); + return state; + } + if (name === "Interrupt") { + if (state.pending) invalidate(); + return state; + } + if (name !== "PostToolUse") return state; + const recovery = jsonObject(state.recovery), pending = jsonObject(state.pending); + if (!recovery || !jsonObject(recovery.target)) return state; + const pages = Array.isArray(request.pages) ? request.pages : []; + for (const value of pages) { + const page = jsonObject(value); + if (!page || page.schema_version !== "context_capture_page_v1") continue; + const candidate = jsonObject(state.candidate); + if (!state.pending && candidate?.capture_sha256 === recovery.sha256 + && candidate?.target && page.ok === true) continue; // completed-page replay + if (page.ok !== true) { + if (pending) invalidate(); + return state; + } + const original = page.sha256 === recovery.sha256; + const wire = pending && page.sha256 === pending.sha256; + if (!original && !wire) continue; // late page for another immutable capture + const total = original ? recovery.total_bytes : pending?.total_bytes; + const offset = page.offset, end = page.eof === true ? total : page.next_offset; + if (!Number.isSafeInteger(total) || Number(total) <= 0 || page.total_bytes !== total + || typeof page.eof !== "boolean" + || !Number.isSafeInteger(offset) || Number(offset) < 0 || !Number.isSafeInteger(end) + || Number(end) <= Number(offset) || Number(end) > Number(total) + || typeof page.content !== "string" + || Buffer.byteLength(page.content, "utf8") !== Number(end) - Number(offset) + || (page.eof === true && page.next_offset !== null) + || (page.eof === false && Number(end) >= Number(total))) { + invalidate(); + return state; + } + const reads = jsonObject(state.pending) ?? {target: recovery.target}; + const all = jsonObject(reads.ranges) ?? {}; + const key = String(page.sha256); + const ranges = mergedRanges(all[key], Number(offset), Number(end)); + all[key] = ranges; + reads.ranges = all; + state.pending = reads; + if (ranges.length !== 1 || ranges[0][0] !== 0 || ranges[0][1] !== total) continue; + const target = {...requireJsonObject(recovery.target, "capture target"), context_epoch: state.context_epoch}; + state.candidate = {basis: NATIVE_CONTEXT_BASIS, target, + capture_file: recovery.path, capture_sha256: recovery.sha256, + ancestry: original ? 0 : pending?.ancestry ?? 0}; + delete state.pending; + } + return state; +} diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 45d56408a5..a8ac68ed44 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -119,19 +119,28 @@ def prepare_context_delivery( context: Mapping[str, Any], *, scope: Mapping[str, str], capture_id: str, baseline: Mapping[str, Any] | None = None, retained: Mapping[str, Any] | None = None, + continuity: Mapping[str, Any] | None = None, surface: str = "work_context", ) -> dict[str, Any]: - """Prepare at a Host's final input boundary, never from CLI self-attestation. + """Prepare under the Host's explicit continuity basis, not a model assertion. Quota and Turn hosts pass their complete immutable projection (or the shared work_context) and its capture id. Surface binding prevents cross-projection reuse. Unsupported hosts omit retention and keep complete delivery. A candidate is - not an ACK: only intact final model-input assembly may confirm its target. + not an ACK. Confirmed input and native delivery assumptions stay distinct. """ return _turn_envelope_result("context_delivery", context=dict(context), scope=dict(scope), capture_id=capture_id, surface=surface, baseline=dict(baseline) if baseline is not None else None, - retained=dict(retained) if retained is not None else None) + retained=dict(retained) if retained is not None else None, + continuity=dict(continuity) if continuity is not None else None) + + +def update_native_context_lifecycle(state: Mapping[str, Any], event: Mapping[str, Any], *, + new_epoch: str, pages: list[dict[str, Any]]) -> dict[str, Any]: + """Native IO adapter; the shared typed owner reduces continuity assumptions.""" + return _turn_envelope_result("native_context_lifecycle", state=dict(state), + event=dict(event), new_epoch=new_epoch, pages=pages) def restore_context_delivery( diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index de301bb4a4..6373bf9fb3 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto"; import { ENVELOPED_SHA256_PATTERN } from "../content_digest.ts"; import {omitUnboundExploreResultWriteback, referenceExploreResultWriteback} from "../capabilities/explore_turn_context.ts"; import {prepareContextDelivery, restoreContextDelivery} from "./context_delivery.ts"; +import {updateNativeContextLifecycle} from "./native_context_lifecycle.ts"; import { interpretQuotaShouldRunPacket, @@ -1276,6 +1277,7 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { } if (operation === "build") return buildTurnEnvelope(request); if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); + if (operation === "native_context_lifecycle") return updateNativeContextLifecycle(request); if (operation === "context_recovery") return restoreContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); if (operation === "bind_capture") return bindTurnDecisionCapture(request); if (operation === "replan_settlement_identity") return {identity: turnEnvelopeReplanSettlementIdentity( diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index d8b280841a..dabe213a2d 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -1,13 +1,13 @@ """Opt-in native lifecycle IO for the existing quota context projector. -The private directory belongs to one native session/workspace. Files are only -candidate references: actual native input must independently prove retention. -No hook grants execution authority or acknowledges model understanding. +The private directory belongs to one native session/workspace. Completed reader +pages establish an explicit continuity assumption; hooks invalidate known loss. +This neither proves retained model input nor grants execution authority. """ from __future__ import annotations from contextlib import contextmanager -from collections.abc import Iterator, Mapping +from collections.abc import Iterator from typing import Any import json import os @@ -23,6 +23,7 @@ # settings are presentation opt-ins, never host authentication or authority. CONTEXT_DELIVERY_SETTINGS = ("off", "on") CONTEXT_DELIVERY_SCENARIOS = ("codex_benchmark_heartbeat", "codex_app_heartbeat") +CONTEXT_LIFECYCLE_HOOKS = ("SessionStart", "PreCompact", "PostCompact", "PostToolUse", "Interrupt") def context_delivery_for_arguments(args: Any, *, codex_machine_preference: bool = False) -> dict[str, Any]: @@ -218,26 +219,6 @@ def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_i ) -def codex_context_transport_configuration(native_config: Mapping[str, Any]) -> dict[str, Any]: - """Keep the default OpenAI request observable without changing its login. - - Native WebSocket requests may carry only previous_response_id and new - items. The input observer deliberately cannot certify that implicit history. - A separate native HTTP provider retains Codex's OpenAI routing and auth; - explicit operator providers are left alone. This config grants no authority - and does not assert that any particular input was complete. - """ - # Native config/read serializes an unset default as null. - if native_config.get("model_provider") not in (None, "openai"): - return {} - provider = "loopx_context_http" - if provider in (native_config.get("model_providers") or {}): - raise ValueError("context HTTP provider conflicts with existing native configuration") - return {"model_provider": provider, "model_providers": {provider: { - "name": "OpenAI", "wire_api": "responses", "requires_openai_auth": True, - "supports_websockets": False}}} - - def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, cli_argv: list[str], scenario: str, session_root: bool = False, inherit_machine: bool = False) -> dict[str, Any]: @@ -245,8 +226,7 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, This renders no trust hashes and writes no host configuration. The native host remains responsible for reviewing/trusting the exact discovered hook. - This manual template assumes the default OpenAI provider. Automatic callers - resolve the effective provider with codex_context_transport_configuration. + Authentication, endpoints, providers and connection modes remain native. """ import shlex @@ -262,26 +242,32 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, *(["--codex-app"] if scenario == "codex_app_heartbeat" else ["--runtime-profile", "generic_cli"]), option, directory]), "timeout": 10} - return {**codex_context_transport_configuration({}), - "hooks": {event: [{"hooks": [handler]}] for event in ("SessionStart", "PreToolUse")}, + return {"hooks": {event: [{**({"matcher": "^Bash$"} if event == "PostToolUse" else {}), + "hooks": [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)} def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str, Any]: - """Consume native lifecycle events; emit only same-capture recovery refs. + """Native IO around the shared typed lifecycle owner, without input tracing. - Compaction/fork/restart invalidate. Same-session resume keeps only a candidate: - the new active call's actual input must prove every byte before reuse. - PreToolUse supplies the native call id; it is not retention proof. Failure to update the - primary journal still returns the existing recovery ref when available. - Trust/enablement and complete event delivery remain native host concerns. + PostToolUse supplies completed reader pages. Assuming their normal delivery + is intentional; an unseen downstream loss or missed hook can escape detection. + Known invalidation/IO failures cannot leave a usable stale baseline. """ + from ..quota.turn_envelope import update_native_context_lifecycle + from ..effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError + if directory is None: return {} name, session = event.get("hook_event_name"), event.get("session_id") if not isinstance(session, str) or not session: return {} + if isinstance(event.get("agent_id"), str) and event["agent_id"]: + return {} # no parent recovery reference may escape to a subagent + pages = _context_pages(event.get("tool_response")) if name == "PostToolUse" else [] + if name == "PostToolUse" and not pages: + return {} # ordinary tool use neither invalidates nor rewrites the journal recovery = None try: with _locked(directory) as root: @@ -289,30 +275,25 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str same = state.get("session_id") == session and state.get("cwd") == event.get("cwd") if same: recovery = state.get("recovery") - else: - state = {"session_id": session, "cwd": event.get("cwd")} - resumed = name == "SessionStart" and event.get("source") == "resume" and same - if resumed: - state["active_call"] = None - elif name in ("SessionStart", "PreCompact"): - state.update(context_epoch=uuid.uuid4().hex, active_call=None) - state.pop("candidate", None) - elif name == "PreToolUse" and same and state.get("context_epoch"): - state["active_call"] = event.get("tool_use_id") - else: - return {} - _save(root, state) - except (OSError, ValueError, TypeError, RecursionError): - # A failed primary update cannot suppress a known full-read reference. - # It also cannot manufacture a fresh active-call observation. - pass - if name == "SessionStart" and isinstance(recovery, dict) and recovery.get("command"): - if event.get("source") == "resume": - return {"hookSpecificOutput": {"hookEventName": name, "additionalContext": - "LoopX resumed context is unconfirmed. Reuse prior context only when complete in current input. " - "If missing before continuing the captured decision, read the original full capture. " - "Recovery does not renew authority or freshness. " + CONTEXT_CAPTURE_READ_GUIDANCE + " " - + recovery["command"]}} + try: + updated = update_native_context_lifecycle(state, event, + new_epoch=uuid.uuid4().hex, pages=pages) + except (EffectRuntimeRemoteError, EffectRuntimeStartupError): + _discard_recovery(root) + raise + if updated != state: + try: + _save(root, updated) + except (OSError, ValueError): + # The explicit recovery reference remains readable, but a + # failed invalidation must not leave the old candidate live. + _discard_recovery(root) + raise + recovery_needed = same and not updated.get("candidate") + except (OSError, ValueError, TypeError, RecursionError, EffectRuntimeRemoteError, EffectRuntimeStartupError): + recovery_needed = True + if (name == "SessionStart" and recovery_needed + and isinstance(recovery, dict) and recovery.get("command")): return {"hookSpecificOutput": {"hookEventName": name, "additionalContext": "LoopX context baseline invalidated. Before dependent work, read this original full capture. " "Capture recovery does not renew authority or source freshness. " @@ -321,39 +302,31 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str return {} -def _trace_bundle(session: str, tool_id: str) -> str | None: - from .codex_input_trace import read_native_trace_file +def _context_pages(value: object) -> list[dict[str, Any]]: + """Decode native shell/MCP JSON output; never classify arbitrary prose. - value = os.environ.get("CODEX_ROLLOUT_TRACE_ROOT") - if not value: - return None # never enable tracing implicitly - matches = [] - # Native diagnostics are optional. Bound discovery instead of scanning a - # long session archive. Resume can create another bundle for the same - # session; the active-call/input verifier, never mtime, selects its source. - for index, path in enumerate(Path(value).glob("trace-*")): - if index >= 128: - return None + Incomplete JSON has no successful page. The pending delivery stays pending. + Native output truncation that is not reported here remains an assumption gap. + """ + if isinstance(value, str): try: - manifest = _read(path / "manifest.json", private=False) - except (OSError, ValueError): - continue - if manifest.get("root_thread_id") == session and manifest.get("rollout_id") == session: - try: - raw = read_native_trace_file(path / "trace.jsonl", tail=True) - if not raw.endswith(b"\n"): - return None - events = [json.loads(line)["payload"] for line in raw.splitlines()] - calls = [event for event in events if event.get("tool_call_id") == tool_id - and event.get("type") in ("tool_call_started", "tool_call_ended")] - if calls and calls[-1]["type"] == "tool_call_started": - matches.append(str(path)) - except (OSError, ValueError, KeyError, TypeError, AttributeError): - return None - # Count live call identities before checking retained content. Otherwise an - # abandoned old call could win when a resumed call with the same id lost its - # input. The selected bundle still needs the complete independent verifier. - return matches[0] if len(matches) == 1 else None + value = json.loads(value) + except ValueError: + return [] + if not isinstance(value, dict): + return [] + if value.get("schema_version") == "context_capture_page_v1": + return [value] + if isinstance(value.get("output"), str): + pages = _context_pages(value["output"]) + if pages and "exit_code" in value and value["exit_code"] != 0: + return [{"schema_version": "context_capture_page_v1", "ok": False}] + return pages + content = value.get("content") + if isinstance(content, list): + return [page for item in content if isinstance(item, dict) + for page in _context_pages(item.get("text"))] + return [] def _discard_recovery(root: Path) -> None: @@ -370,33 +343,48 @@ def _discard_recovery(root: Path) -> None: def _native_context_request(state: dict[str, Any], scope: dict[str, Any]) -> dict[str, Any]: - """A saved candidate is reusable only with independent current-input proof.""" - from .codex_input_trace import codex_context_in_active_input - + """Use the explicit completed-delivery assumption, never a fabricated ACK.""" request: dict[str, Any] = {"scope": scope} candidate = state.get("candidate") - if isinstance(candidate, dict) and isinstance(state.get("active_call"), str): + if (isinstance(candidate, dict) and candidate.get("basis") == "native_hook_assumption" + and not state.get("pending")): try: - baseline = _read(Path(candidate["path"])) - trace = _trace_bundle(scope["session_id"], state["active_call"]) - if trace: - observation = dict(trace_bundle=trace, tool_call_id=state["active_call"], - capture_file=candidate["capture_file"], capture_sha256=candidate["capture_sha256"]) - if codex_context_in_active_input(observation, baseline=baseline, session_id=scope["session_id"]): - request.update(baseline=baseline, retained={"complete": True, "target": baseline["target"]}, - native_input=observation) + path = Path(candidate["capture_file"]) + payload = _read(path) + # The shared codec independently validates the complete context + # digest and scope. No second read or full native-history scan. + baseline = dict(schema_version="context_delivery_v1", mode="full", + target=candidate["target"], ancestry=candidate.get("ancestry", 0), context=payload) + request.update(baseline=baseline, + continuity={"basis": "native_hook_assumption", "target": candidate["target"]}) except (OSError, ValueError, KeyError, TypeError, RecursionError): pass # Missing candidate affects reuse, never the current recovery reference. return request +def register_native_context_view(directory: str | None, view: dict[str, Any]) -> None: + """Bind the existing pending delivery to its actual paged wire, under lock.""" + if directory is None or not isinstance(view.get("target"), dict): + return + with _locked(directory) as root: + state = _state(root) + pending = state.get("pending") + if (not isinstance(pending, dict) or pending.get("target") != view["target"] + or state.get("session_id") != os.environ.get("CODEX_THREAD_ID")): + raise ValueError("native page view does not match the current pending capture") + pending.update(sha256=view["sha256"], total_bytes=view["total_bytes"], + ancestry=view.get("ancestry", 0)) + _save(root, state) + + def project_codex_context(payload: dict[str, Any], *, directory: str | None, capture_directory: Path, - goal_id: str, agent_id: str, surface: str, capture_id: str) -> dict[str, Any]: + goal_id: str, agent_id: str, surface: str, capture_id: str, + continuity_generation: str | None = None) -> dict[str, Any]: """Commit current recovery before optional projection or candidate updates. Serialization prevents invalidation from moving the epoch during preparation. Full fallback must recover this capture or refuse lookup, never an older one. - Candidates still need independent current-input proof before reuse. + Completed full or delta pages establish the explicit native-hook assumption. """ from ..quota.error_codes import QuotaDecisionTransportError @@ -419,21 +407,27 @@ def project_codex_context(payload: dict[str, Any], *, directory: str | None, cap raise ValueError("context recovery requires the same native session and workspace") if not isinstance(epoch, str) or not epoch: raise ValueError("context recovery requires a native context epoch") + scope = dict(goal_id=goal_id, agent_id=agent_id, session_id=session, context_epoch=epoch) + if continuity_generation is not None: + scope["continuity_generation"] = continuity_generation + request = _native_context_request(state, scope) recovery = capture_context_projection(payload, directory=capture_directory) state["recovery"] = {**{key: recovery[key] for key in ("path", "sha256", "command")}, - "goal_id": goal_id, "agent_id": agent_id} + "goal_id": goal_id, "agent_id": agent_id, + "total_bytes": Path(recovery["path"]).stat().st_size} + # Block reuse before optional wire IO, not after a first page + # or EOF. A codec/read interruption keeps this incomplete. + state["pending"] = {"sha256": recovery["sha256"], + "total_bytes": state["recovery"]["total_bytes"], "ranges": {}} _save(root, state) recovery_committed = True - scope = dict(goal_id=goal_id, agent_id=agent_id, session_id=session, context_epoch=epoch) result = project_captured_context(payload, directory=capture_directory, - request=_native_context_request(state, scope), captured_recovery=recovery, + request=request, captured_recovery=recovery, goal_id=goal_id, agent_id=agent_id, surface=surface, capture_id=capture_id) - if result.get("schema_version") == "context_delivery_v1" and result["mode"] == "full": - path = capture_directory / "presentation.json" - write_decision_capture(path, result) - state["candidate"] = dict(path=str(path), capture_file=recovery["path"], - capture_sha256=recovery["sha256"]) + if result.get("schema_version") == "context_delivery_v1": + state["recovery"]["target"] = result["target"] + state["pending"].update(target=result["target"], ancestry=result["ancestry"]) _save(root, state) return result except (OSError, ValueError, KeyError, TypeError, RecursionError): diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 26cc1230f5..8a1440a7a0 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -145,8 +145,8 @@ def project_captured_context( """Adapt an admitted capture to the shared TS presentation owner. The caller-owned rendezvous may supply a continuity observation. It must - still recheck the candidate at final model-input assembly and ACK only intact - delivery. Unsupported ordinary CLI clients never supply this request. Every + either confirm intact final input or explicitly select native lifecycle + assumptions. Unsupported ordinary CLI clients never supply this request. Every opt-in attempt saves the complete projected JSON for same-capture recovery; neither malformed requests nor optimization failures rerun the guard. A native caller may pass its already-published recovery reference, never a @@ -164,6 +164,7 @@ def project_captured_context( if not isinstance(scope, dict) or scope.get("goal_id") != goal_id or scope.get("agent_id") != agent_id: return payload retained = request.get("retained") + continuity = request.get("continuity") if "native_input" in request: from .codex_input_trace import codex_context_in_active_input @@ -177,9 +178,10 @@ def project_captured_context( or not codex_context_in_active_input(observation, baseline=baseline, session_id=native_session)): retained = None + continuity = None delivery = prepare_context_delivery(payload, scope=scope, capture_id=capture_id, surface=surface, - baseline=request.get("baseline"), retained=retained) + baseline=request.get("baseline"), retained=retained, continuity=continuity) delivery["recovery"] = recovery return delivery except (OSError, ValueError, TypeError, EffectRuntimeRemoteError, EffectRuntimeStartupError): diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index f1b603ac6e..a3b7a6716e 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -72,7 +72,7 @@ def _delivered(request, digest): def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None): isolate_sqlite_runtime(tmp_path, monkeypatch) binary, env, workspace, home = _native_fixture(tmp_path) - env["CODEX_ROLLOUT_TRACE_ROOT"] = str(workspace / "traces") + env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) env.update(NODE_OPTIONS="--experimental-sqlite", LOOPX_USAGE_PING="0") project, runtime, registry = _write_fixture(workspace) state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" @@ -134,8 +134,11 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho hook_command = shlex.join([str(wrapper), "quota", "context-hook", "--context-delivery", "on", "--codex-app", context_option, str(lifecycle)]) handler = {"type": "command", "command": hook_command, "timeout": 10} if host != "benchmark": + from loopx.control_plane.turn_driver.codex_context import CONTEXT_LIFECYCLE_HOOKS + (home / "hooks.json").write_text(json.dumps({"hooks": { - event: [{"hooks": [handler]}] for event in ("SessionStart", "PreToolUse")}})) + event: [{**({"matcher": "^Bash$"} if event == "PostToolUse" else {}), + "hooks": [handler]}] for event in CONTEXT_LIFECYCLE_HOOKS}})) errors, snapshots = [], [] stages = {"phase": "start", "pages": [], "baseline_confirmed": False, "compact_response": None} wires, page_digests, read_paths = {}, {}, {} diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 81cf6fc959..0bbc5bac74 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -1,4 +1,4 @@ -"""Native lifecycle references cannot stand in for retained model input.""" +"""Native delivery assumptions, invalidation, recovery and scope isolation.""" from __future__ import annotations import json @@ -64,7 +64,9 @@ def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_p assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) native = payload["native_config"] - assert set(native["hooks"]) == {"SessionStart", "PreToolUse"} + assert set(native["hooks"]) == set(context.CONTEXT_LIFECYCLE_HOOKS) + assert "model_provider" not in native and "model_providers" not in native + assert "ROLLOUT_TRACE" not in json.dumps(payload) assert "--codex-context-root" in native["developer_instructions"] assert "context-read" in native["developer_instructions"] assert "hooks.state" not in native and list(tmp_path.iterdir()) == [] @@ -73,23 +75,6 @@ def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_p assert missing_identity.returncode == 1 and not json.loads(missing_identity.stdout)["ok"] -@pytest.mark.parametrize("native_config", [{}, {"model_provider": None, "model_providers": None}, - {"model_provider": "openai"}]) -def test_context_transport_retains_native_auth_and_explicit_provider_ownership(native_config): - original = {"model_provider": "operator", "model_providers": {"operator": { - "base_url": "https://example.invalid", "supports_websockets": True}}} - assert context.codex_context_transport_configuration(original) == {} - assert original["model_providers"]["operator"]["supports_websockets"] is True - native = context.codex_context_transport_configuration(native_config) - selected = native["model_provider"] - assert native["model_providers"][selected] == { - "name": "OpenAI", "wire_api": "responses", "requires_openai_auth": True, - "supports_websockets": False} - with pytest.raises(ValueError, match="conflicts"): - context.codex_context_transport_configuration({"model_providers": { - selected: {"base_url": "https://example.invalid"}}}) - - def setup(root, monkeypatch): root.chmod(0o700) monkeypatch.chdir(root) @@ -100,18 +85,19 @@ def setup(root, monkeypatch): return event -def project(root, capture_id="one", *, payload=None): +def project(root, capture_id="one", *, payload=None, generation=None): directory = root / capture_id directory.mkdir(mode=0o700) if payload is None: payload = {"ok": True, "goal_id": "fixture", "requirements": "Preserve final stop condition."} result = context.project_codex_context(payload, directory=str(root), capture_directory=directory, - goal_id="fixture", agent_id="agent", surface="quota_packet", capture_id=capture_id) + goal_id="fixture", agent_id="agent", surface="quota_packet", capture_id=capture_id, + continuity_generation=generation) return payload, result @pytest.mark.parametrize("event_name,source", [("SessionStart", "compact"), - ("SessionStart", "startup"), ("PreCompact", None)]) + ("SessionStart", "startup"), ("PreCompact", None), ("PostCompact", None)]) def test_lifecycle_invalidates_and_recovers_current_capture_without_bodies(tmp_path, monkeypatch, event_name, source): event = setup(tmp_path, monkeypatch) payload, first = project(tmp_path) @@ -131,66 +117,133 @@ def test_lifecycle_invalidates_and_recovers_current_capture_without_bodies(tmp_p assert second["mode"] == "full" and second["target"]["context_epoch"] == after["context_epoch"] -def test_resume_keeps_only_an_unconfirmed_candidate_and_never_borrows_old_call(tmp_path, monkeypatch): +def read_pages(root, wire, *, size=3072): + import argparse + from loopx.cli_commands.quota_capture import native_context_capture_view, read_context_capture + + view = native_context_capture_view(wire, Path(wire["recovery"]["path"]).parent, + context_directory=str(root)) + path, digest = view["path"], view["sha256"] + pages = [] + offset = 0 + while True: + page = read_context_capture(argparse.Namespace(capture_file=path, + capture_sha256=digest, offset=offset, page_bytes=size)) + assert page["ok"] + pages.append(page) + if page["eof"]: + break + offset = page["next_offset"] + return pages + + +def deliver(root, event, pages): + for page in pages: + assert context.codex_context_hook(str(root), {**event, "hook_event_name": "PostToolUse", + "tool_use_id": f"page-{page.get('offset')}", "tool_response": json.dumps(page)}) == {} + + +def test_complete_reader_delivery_allows_delta_without_diagnostics_or_ack(tmp_path, monkeypatch): event = setup(tmp_path, monkeypatch) - _, first = project(tmp_path) - context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": "PreToolUse", "tool_use_id": "old"}) - before = context._state(tmp_path) - result = context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) - after = context._state(tmp_path) - assert after["candidate"] == before["candidate"] and after["context_epoch"] == before["context_epoch"] - assert after["active_call"] is None - assert first["recovery"]["command"] in result["hookSpecificOutput"]["additionalContext"] - assert "unconfirmed" in result["hookSpecificOutput"]["additionalContext"] - # No current-input proof: even a complete private candidate remains full. - _, second = project(tmp_path, "two") + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, first = project(tmp_path, payload=payload) + assert "candidate" not in context._state(tmp_path) + deliver(tmp_path, event, read_pages(tmp_path, first)) + state = context._state(tmp_path) + assert state["candidate"]["basis"] == "native_hook_assumption" and "pending" not in state + assert "retained" not in state and "active_call" not in state + assert context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) == {} + # Ordinary tools and normal turn endings preserve the completed baseline. + for name in ("PreToolUse", "PostToolUse", "Stop", "SessionEnd"): + context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": name, + "tool_response": "ordinary tool output"}) + assert context._state(tmp_path) == state + _, second = project(tmp_path, "two", payload=payload) + assert second["mode"] == "delta" and second["continuity_basis"] == "native_hook_assumption" + assert not (tmp_path / "traces").exists() + deliver(tmp_path, event, read_pages(tmp_path, second)) + _, third = project(tmp_path, "three", payload=payload) + assert third["mode"] == "delta" + + +@pytest.mark.parametrize("failure", ["tail_only", "gap", "truncated", "interrupted", "reader_error", "failed_tool"]) +def test_known_incomplete_delivery_cannot_establish_baseline(tmp_path, monkeypatch, failure): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, first = project(tmp_path, payload=payload) + pages = read_pages(tmp_path, first) + if failure == "tail_only": + deliver(tmp_path, event, pages[-1:]) # EOF alone is insufficient + elif failure == "gap": + deliver(tmp_path, event, pages[:1] + pages[2:]) + elif failure == "truncated": + deliver(tmp_path, event, [{**pages[0], "content": pages[0]["content"][:-1]}]) + elif failure == "interrupted": + deliver(tmp_path, event, pages[:1]) + context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": "Interrupt"}) + elif failure == "failed_tool": + context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": "PostToolUse", + "tool_response": {"exit_code": 1, "output": json.dumps(pages[0])}}) + else: + deliver(tmp_path, event, [{"ok": False, "schema_version": "context_capture_page_v1"}]) + assert "candidate" not in context._state(tmp_path) + _, second = project(tmp_path, "two", payload=payload) assert second["mode"] == "full" + assert second["recovery"]["path"] != first["recovery"]["path"] + assert json.loads(Path(first["recovery"]["path"]).read_bytes()) == payload -@pytest.mark.parametrize("case", ["closed_old", "duplicate_live", "abandoned_old", "missing_input", "discovery_overflow"]) -def test_resumed_bundle_needs_unique_current_input_not_latest_file(tmp_path, monkeypatch, case): - from test_codex_input_retention import fixture +def test_parallel_and_duplicate_pages_are_idempotent_and_cannot_borrow_other_scope(tmp_path, monkeypatch): + from concurrent.futures import ThreadPoolExecutor event = setup(tmp_path, monkeypatch) - requirements = "Preserve the whole source and final stop condition. " * 1000 - payload = {"ok": True, "goal_id": "fixture", "plan_summary": {"requirements": requirements}} + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} _, first = project(tmp_path, payload=payload) - traces = tmp_path / "traces" - traces.mkdir() - for number in (1, 2): - parent = tmp_path / str(number) - parent.mkdir() - observation, _, _, rows = fixture(parent) - bundle = Path(observation["trace_bundle"]) - if number == 1 and case not in {"duplicate_live", "abandoned_old"}: - rows.append({**rows[-1], "seq": 3, "payload": {"type": "tool_call_ended", "tool_call_id": "guard"}}) - (bundle / "trace.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows)) - missing = case == "missing_input" or (case == "abandoned_old" and number == 2) - (bundle / "payloads/input.json").write_text(json.dumps({"input": [] if missing else [ - {"type": "function_call_output", "output": json.dumps(first)}]})) - bundle.rename(traces / f"trace-{number}") - if case == "discovery_overflow": - for number in range(3, 130): - (traces / f"trace-{number}").mkdir() - monkeypatch.setenv("CODEX_ROLLOUT_TRACE_ROOT", str(traces)) - context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) - context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": "PreToolUse", "tool_use_id": "guard"}) + pages = read_pages(tmp_path, first) + # A subagent's parent session id and another native session never promote it. + for extra in ({"agent_id": "child"}, {"session_id": "other"}, {"cwd": str(tmp_path.parent)}): + deliver(tmp_path, {**event, **extra}, pages) + assert "candidate" not in context._state(tmp_path) + with ThreadPoolExecutor(max_workers=4) as pool: + list(pool.map(lambda page: deliver(tmp_path, event, [page]), reversed(pages))) + state = context._state(tmp_path) + assert state["candidate"]["basis"] == "native_hook_assumption" + deliver(tmp_path, event, pages) + assert context._state(tmp_path) == state _, second = project(tmp_path, "two", payload=payload) - assert second["mode"] == ("delta" if case == "closed_old" else "full") + assert second["mode"] == "delta" -def test_missing_observer_or_hook_never_reuses_candidate_and_other_sessions_do_not_borrow(tmp_path, monkeypatch): +def test_compaction_reset_recovers_the_same_original_before_new_delta(tmp_path, monkeypatch): event = setup(tmp_path, monkeypatch) - _, first = project(tmp_path) - context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": "PreToolUse", "tool_use_id": "current"}) - _, second = project(tmp_path, "two") - assert second["mode"] == "full" # a present file plus a hook is not proof - assert second["target"]["capture_id"] != first["target"]["capture_id"] - monkeypatch.setenv("CODEX_THREAD_ID", "another-session") - payload, rejected = project(tmp_path, "other") - assert rejected == payload - assert context.codex_context_hook(str(tmp_path), {**event, "session_id": "another-session", "source": "fork"}) == {} - assert "recovery" not in context._state(tmp_path) and "candidate" not in context._state(tmp_path) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, first = project(tmp_path, payload=payload) + pages = read_pages(tmp_path, first) + deliver(tmp_path, event, pages) + old_epoch = context._state(tmp_path)["context_epoch"] + for name in ("PreCompact", "PostCompact", "SessionStart"): + reply = context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": name, "source": "compact"}) + reset = context._state(tmp_path) + assert reset["context_epoch"] != old_epoch and "candidate" not in reset + assert first["recovery"]["sha256"] in reply["hookSpecificOutput"]["additionalContext"] + # The unchanged original reader, not a fresh guard, restores the baseline. + deliver(tmp_path, event, pages) + assert context._state(tmp_path)["candidate"]["target"]["context_epoch"] == reset["context_epoch"] + _, second = project(tmp_path, "two", payload=payload) + assert second["mode"] == "delta" + + +def test_configuration_round_trip_requires_a_new_full_baseline(tmp_path, monkeypatch): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, first = project(tmp_path, payload=payload, generation="enabled-before-off") + deliver(tmp_path, event, read_pages(tmp_path, first)) + _, second = project(tmp_path, "two", payload=payload, generation="enabled-after-off") + assert second["mode"] == "full" and second["reason"] == "baseline_unavailable" + deliver(tmp_path, event, read_pages(tmp_path, second)) + _, third = project(tmp_path, "three", payload=payload, generation="enabled-after-off") + assert third["mode"] == "delta" + def test_primary_write_failure_preserves_small_recovery_and_does_not_create_ack(tmp_path, monkeypatch): @@ -202,7 +255,8 @@ def unavailable(*args, **kwargs): monkeypatch.setattr(context, "_save", unavailable) result = context.codex_context_hook(str(tmp_path), {**event, "source": "compact"}) assert wire["recovery"]["sha256"] in result["hookSpecificOutput"]["additionalContext"] - assert (tmp_path / "session.json").read_bytes() == before + assert not (tmp_path / "session.json").exists() + assert Path(wire["recovery"]["path"]).exists() def _assert_recovery_after_fallback(root, event, source, capture): @@ -225,11 +279,8 @@ def _assert_recovery_after_fallback(root, event, source, capture): @pytest.mark.parametrize("source", ["compact", "resume"]) -def test_oversized_warm_request_keeps_current_full_recovery(tmp_path, monkeypatch, source): - from test_codex_input_retention import fixture - from loopx.control_plane.effect_runtime import EffectRuntimeRejected, MAX_REQUEST_BYTES - from loopx.control_plane.quota.turn_envelope import prepare_context_delivery - from loopx.control_plane.turn_driver.codex_input_trace import codex_context_in_active_input +def test_large_capture_keeps_current_full_recovery(tmp_path, monkeypatch, source): + from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES event = setup(tmp_path, monkeypatch) before = {"ok": True, "goal_id": "fixture", "stop": "Stop before publication.", @@ -238,31 +289,10 @@ def test_oversized_warm_request_keeps_current_full_recovery(tmp_path, monkeypatc _, first = project(tmp_path, payload=before) original = Path(first["recovery"]["path"]).read_bytes() assert len(original) < MAX_REQUEST_BYTES - traces = tmp_path / "traces" - traces.mkdir() - observation, _, _, _ = fixture(traces) - bundle = Path(observation["trace_bundle"]) - # Actual production verifier consumes these synthetic native trace files; - # neither the projector nor the RPC size limit is mocked. - request = bundle / "payloads/input.json" - request.write_text(json.dumps({"input": [{"type": "function_call_output", "output": json.dumps(first)}]})) - assert request.stat().st_size < MAX_REQUEST_BYTES - current_bundle = traces / "trace-live" - bundle.rename(current_bundle) - observation.update(trace_bundle=str(current_bundle), capture_file=first["recovery"]["path"], - capture_sha256=first["recovery"]["sha256"]) - assert codex_context_in_active_input(observation, baseline=first, session_id="session") - monkeypatch.setenv("CODEX_ROLLOUT_TRACE_ROOT", str(traces)) - context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": "PreToolUse", "tool_use_id": "guard"}) - scope = {key: first["target"][key] for key in ("goal_id", "agent_id", "session_id", "context_epoch")} - with pytest.raises(EffectRuntimeRejected) as rejected: - prepare_context_delivery(after, scope=scope, capture_id="two", surface="quota_packet", - baseline=first, retained={"complete": True, "target": first["target"]}) - assert rejected.value.diagnostic_code == "request_too_large" + deliver(tmp_path, event, read_pages(tmp_path, first, size=8192)) _, delivered = project(tmp_path, "two", payload=after) - assert delivered.get("context", delivered) == after + assert delivered["mode"] == "delta" current_capture = tmp_path / "two/projection.json" - assert current_capture.stat().st_size < MAX_REQUEST_BYTES assert json.loads(current_capture.read_bytes()) == after _assert_recovery_after_fallback(tmp_path, event, source, current_capture) assert Path(first["recovery"]["path"]).read_bytes() == original @@ -279,7 +309,7 @@ def test_journal_failure_never_recovers_previous_capture(tmp_path, monkeypatch, def fail_current_write(root, state): current_recovery = state.get("recovery", {}).get("sha256") not in (None, first["recovery"]["sha256"]) - current_candidate = state.get("candidate", {}).get("capture_file") == str(current_capture) + current_candidate = isinstance(state.get("recovery", {}).get("target"), dict) if current_recovery and (failed_write == "recovery" or current_candidate): failures.append(failed_write) raise OSError("synthetic current publication unavailable") diff --git a/tests/control_plane/test_context_machine_configuration.py b/tests/control_plane/test_context_machine_configuration.py index 98e6ad75b3..b978aa8e3a 100644 --- a/tests/control_plane/test_context_machine_configuration.py +++ b/tests/control_plane/test_context_machine_configuration.py @@ -193,11 +193,39 @@ def test_machine_on_reaches_real_guard_and_same_capture_readback_without_readmis recovery = wire["recovery"] raw = Path(recovery["path"]).read_bytes() assert hashlib.sha256(raw).hexdigest() == recovery["sha256"] - assert json.loads(raw) == wire["context"] + assert json.loads(raw)["goal_id"] == GOAL_ID + + def complete_reader(view): + page = view + while True: + subprocess.run(shlex.split(hook), input=json.dumps({**event, + "hook_event_name": "PostToolUse", "tool_response": json.dumps(page)}), + text=True, cwd=project, capture_output=True, check=True) + if page["eof"]: + break + code, page = _run_cli(registry, runtime, "quota", "context-read", + "--capture-file", view["path"], "--capture-sha256", view["sha256"], + "--offset", str(page["next_offset"]), "--page-bytes", "3072", cwd=project) + assert code == 0 and page["ok"], page + + complete_reader(wire) + code, warm = _run_cli(registry, runtime, *args, cwd=project) + assert code == 0 and warm["mode"] == "delta", warm + complete_reader(warm) + old_generation = warm["target"]["continuity_generation"] + old_configuration = (runtime / "machine/configuration.json").read_bytes() + assert configure(registry, runtime, tmp_path, "off")[0] == 0 + assert configure(registry, runtime, tmp_path, "on")[0] == 0 + assert (runtime / "machine/configuration.json").read_bytes() == old_configuration + code, fresh = _run_cli(registry, runtime, *args, cwd=project) + assert code == 0 and fresh["mode"] == "full", fresh + assert fresh["target"]["continuity_generation"] != old_generation code, page = _run_cli(registry, runtime, "quota", "context-read", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, cwd=project) - assert code == 0 and page["sha256"] == recovery["sha256"], page - assert page["content"].encode() == raw[:page.get("next_offset") or len(raw)] + latest_raw = Path(fresh["recovery"]["path"]).read_bytes() + assert code == 0 and page["sha256"] == fresh["recovery"]["sha256"], page + assert page["content"].encode() == latest_raw[:page.get("next_offset") or len(latest_raw)] + assert Path(recovery["path"]).read_bytes() == raw assert _heartbeat_receipt_count(runtime, "machine-original-turn") == 1 assert _spend_run_count(runtime) == 0 and state.read_bytes() == original_state # Explicit off wins over machine on, including old ordinary full output. diff --git a/tests/control_plane/test_saved_context_project.py b/tests/control_plane/test_saved_context_project.py index 91c25fcc42..398bcbae18 100644 --- a/tests/control_plane/test_saved_context_project.py +++ b/tests/control_plane/test_saved_context_project.py @@ -297,7 +297,7 @@ def test_preflight_instruction_pins_both_projection_and_full_fallback(tmp_path): @pytest.mark.skipif(not os.environ.get("LOOPX_NATIVE_CHECKPOINT_BIN"), reason="Explicit native Codex; loopback inference only") @pytest.mark.parametrize("host", ["app_server", "benchmark", "app_machine", "app_code_mode"]) -def test_native_saved_preflight_uses_current_input_without_guard_replay(tmp_path, monkeypatch, host): +def test_native_saved_preflight_uses_lifecycle_without_guard_replay(tmp_path, monkeypatch, host): from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime, read_fixture_todos from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.control_plane.quota.turn_envelope import prepare_context_delivery, restore_context_delivery @@ -308,7 +308,8 @@ def test_native_saved_preflight_uses_current_input_without_guard_replay(tmp_path isolate_sqlite_runtime(tmp_path, monkeypatch) binary, env, workspace, home = _native_fixture(tmp_path) - env.update(NODE_OPTIONS="--experimental-sqlite", LOOPX_USAGE_PING="0", CODEX_ROLLOUT_TRACE_ROOT=str(workspace / "traces")) + env.update(NODE_OPTIONS="--experimental-sqlite", LOOPX_USAGE_PING="0") + env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) project, runtime, registry = _write_fixture(workspace) state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" todos = read_fixture_todos(state) @@ -375,7 +376,7 @@ def response(request, number, message): output = [part["text"] for part in output if part.get("type") == "input_text"][-1] descriptor = json.loads(output.split("\nOutput:\n", 1)[-1]) assert descriptor["ok"] and descriptor["mode"] == ("full" if name == "cold" else "delta") - wire = json.loads((Path(descriptor["path"]).parent / "presentation.json").read_text()) if name == "cold" else json.loads(Path(descriptor["path"]).read_text()) + wire = json.loads((Path(descriptor["path"]).parent / "delivery.json").read_text()) assert descriptor["target"] == wire["target"] observed[name] = (wire, descriptor) data = Path(descriptor["path"]).read_bytes() From b9682dc0571ae31c33bbd91484ad0fa5af13a1dc Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:34:24 +0800 Subject: [PATCH 09/40] test(context): qualify lifecycle hooks without diagnostic trace setup Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_boundary.py | 4 +++- tests/control_plane/test_codex_context_capture.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/control_plane/test_codex_context_boundary.py b/tests/control_plane/test_codex_context_boundary.py index 9c30975d07..f14f4657c6 100644 --- a/tests/control_plane/test_codex_context_boundary.py +++ b/tests/control_plane/test_codex_context_boundary.py @@ -92,7 +92,9 @@ def _app_server(binary, env, workspace, home, trusted, additional_context=None, # bypass flag as app-server trust propagation. listed = native.request("hooks/list", {"cwds": [str(workspace)]}) handlers = [h for entry in listed["data"] for h in entry["hooks"]] - assert len(handlers) == 2 + fixture_hooks = json.loads((home / "hooks.json").read_text())["hooks"] + expected_handlers = sum(len(group["hooks"]) for groups in fixture_hooks.values() for group in groups) + assert expected_handlers > 0 and len(handlers) == expected_handlers assert all(h["sourcePath"] == str((home / "hooks.json").resolve()) for h in handlers) params["config"] = {"hooks.state": { h["key"]: {"trusted_hash": h["currentHash"]} for h in handlers}} diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index a3b7a6716e..01ff15fce6 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -507,12 +507,10 @@ def held(*args, **kwargs): pages_in_output += isinstance(page, dict) and page.get("schema_version") == "context_capture_page_v1" visible_batches.append(pages_in_output) assert max(visible_batches, default=0) > 2 # complete bytes were independently checked above - if host == "benchmark": - trace_roots = list((home / "context-traces").glob("wake-*")) - assert len(trace_roots) == (2 if resumed else 1) - assert all(path.stat().st_mode & 0o077 == 0 for path in trace_roots) - elif resumed: - assert len(list((workspace / "traces").glob("trace-*/manifest.json"))) >= 2 + cache_root = home / "loopx-context" if host == "benchmark" else lifecycle + assert cache_root.is_dir() and cache_root.stat().st_mode & 0o077 == 0 + assert not (home / "context-traces").exists() + assert not (workspace / "traces").exists() projection = Path(wires["warm"]["recovery"]["path"]) assert hashlib.sha256(projection.read_bytes()).hexdigest() == wires["warm"]["recovery"]["sha256"] current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, From 877a8847b0340e2accade02e0ae9862ddea504c6 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:49:31 +0800 Subject: [PATCH 10/40] test(context): allow complete multi-stage native recovery fixtures Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_boundary.py | 5 +++-- tests/control_plane/test_codex_context_capture.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/control_plane/test_codex_context_boundary.py b/tests/control_plane/test_codex_context_boundary.py index f14f4657c6..e9aada455f 100644 --- a/tests/control_plane/test_codex_context_boundary.py +++ b/tests/control_plane/test_codex_context_boundary.py @@ -77,7 +77,8 @@ def _input_text(request): return "\n".join(text) -def _app_server(binary, env, workspace, home, trusted, additional_context=None, *, sandbox="read-only", resume=None): +def _app_server(binary, env, workspace, home, trusted, additional_context=None, *, sandbox="read-only", resume=None, + turn_timeout_sec=30): with (home / "stderr.txt").open("w") as stderr, StdioNativeGoalTransport.spawn( [binary, "app-server", "--listen", "stdio://"], cwd=str(workspace), env=env, stderr=stderr, response_timeout_sec=20, @@ -106,7 +107,7 @@ def _app_server(binary, env, workspace, home, trusted, additional_context=None, if additional_context is not None: turn_params["additionalContext"] = additional_context turn = native.request("turn/start", turn_params)["turn"]["id"] - deadline = time.monotonic() + 30 + deadline = time.monotonic() + turn_timeout_sec while time.monotonic() < deadline: event = native.next_event(timeout_sec=1) if event and event.get("method") == "turn/completed": diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 01ff15fce6..dd3e8cf9c8 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -477,10 +477,12 @@ def held(*args, **kwargs): assert all(path.read_bytes() == raw for path, raw in held_captures) assert result["checkpoint"]["captured"] is False # no replan was requested else: - session_id = _app_server(binary, env, workspace, home, True, sandbox="workspace-write") + # This large semantic fixture reads full, delta and recovery pages. + # Allow their native hook/CLI IO; this is not a latency qualification. + session_id = _app_server(binary, env, workspace, home, True, sandbox="workspace-write", turn_timeout_sec=60) if resumed: assert _app_server(binary, env, workspace, home, True, sandbox="workspace-write", - resume=session_id) == session_id + resume=session_id, turn_timeout_sec=60) == session_id assert not errors, errors assert stages["phase"] == "done" From 0e7049f4ff0563f7b87887e7200d489f3602f020 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:06:50 +0800 Subject: [PATCH 11/40] fix(context): verify capture bytes before native baseline promotion Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 17 +- loopx/cli_commands/quota_capture.py | 29 +--- loopx/control_plane/quota/context_delivery.ts | 1 + .../quota/native_context_lifecycle.ts | 13 +- loopx/control_plane/quota/turn_envelope.py | 5 +- .../turn_driver/codex_context.py | 42 ++++- .../turn_driver/codex_input_trace.py | 20 +-- .../turn_driver/decision_capture.py | 51 +++++- .../test_codex_context_lifecycle.py | 148 +++++++++++++++++- 9 files changed, 265 insertions(+), 61 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index e44ba93235..feca8e5d68 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -216,6 +216,15 @@ read, reducing output-wrapper truncation; consume the inline page before followi next_command. Forward exec stdout directly in code mode. Repeating the packet in a hashing tool call does not establish that its original bytes were retained. The reader verifies the immutable capture digest. Native lifecycle delivery uses +the same byte-range validator as explicit input observations: before merging +reader ranges, the post-tool adapter checks actual UTF-8 content against the +trusted journal's digest-pinned physical capture. A changed body, missing or +replaced file, symlink or digest failure cannot advance the baseline. It keeps +the original recovery reference; it does not collect another decision. This +checks output observed at the hook boundary, not downstream model retention. +Older metadata-only candidates require a new complete verified read before +resume or the codec can reuse them; page metadata alone cannot mark them verified. +Native lifecycle continuity still uses an explicit engineering assumption: a normally completed full read was read and remains available until invalidated. This is not a retained-input observation or a model ACK. The shared codec still validates the exact baseline digest, scope, @@ -357,7 +366,13 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 固定摘要的后续读取与完整恢复,不另行准入。该原生读取链的页大小保持 3 KiB,以减少 输出包装造成的截断;先消费已附带页,再跟随 next_command。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 -读取器校验不可变 capture 的摘要。原生路径明确采用工程假设:正常完整交付的首包已读, +读取器校验不可变 capture 的摘要。Post-tool 适配层复用显式输入观察的字节范围校验, +先将实际 UTF-8 正文与可信 journal 指向的物理捕获及固定摘要核对,再合并读取范围。 +正文改变、文件丢失或替换、符号链接及摘要失败均不得晋升基线;保留原始恢复引用, +不重新收集决策。这只验证 hook 边界观察到的输出,不证明下游模型输入保留。 +旧版仅凭页元数据建立的候选须先完整读取并通过字节校验,才能在 resume 或 codec 中复用; +页元数据本身不能写入已校验标记。 +原生连续性仍明确采用工程假设:正常完整交付的首包已读, 在失效事件发生前仍可用;这不是实际输入保留证明或模型 ACK。共享 codec 仍校验精确基线 摘要、作用域、本轮控制与完整重建。显式 final-input Host 客户端保留独立的确认输入契约。 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 6ddedb0496..9df68f1e1b 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -18,6 +18,7 @@ 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_read_command, + read_context_capture_file as _read_capture, ) @@ -30,32 +31,6 @@ def native_context_activation(args: argparse.Namespace) -> dict[str, Any]: raise QuotaCommandValidationError(str(exc)) from exc -def _read_capture(value: str, digest: str) -> tuple[Path, bytes, dict[str, Any]]: - """One bounded physical-file/hash check for recovery and saved projection.""" - from ..control_plane.effect_runtime import MAX_REQUEST_BYTES - - if not isinstance(digest, str) or BARE_SHA256_PATTERN.fullmatch(digest) is None or not value: - raise ValueError("context-read requires the original capture file and its exact SHA-256") - path = Path(value).expanduser().absolute() - # Parent aliases such as macOS /tmp are normal capture locations. - # Resolve those once, but never follow a substituted final file. - path = path.parent.resolve(strict=True) / path.name - if path.is_symlink(): - raise ValueError("context-read requires the physical original capture path, not a symlink") - descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) - with os.fdopen(descriptor, "rb") as stream: - metadata = os.fstat(stream.fileno()) - if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > MAX_REQUEST_BYTES: - raise ValueError("capture must be a regular JSON file within the existing transport size limit") - raw = stream.read(MAX_REQUEST_BYTES + 1) - if len(raw) > MAX_REQUEST_BYTES or hashlib.sha256(raw).hexdigest() != digest: - raise ValueError("capture digest mismatch; restore the original capture, do not rerun admission") - payload = json.loads(raw.decode("utf-8")) - if not isinstance(payload, dict): - raise ValueError("capture must contain a complete JSON object") - return path, raw, payload - - def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: """Bounded local IO over immutable JSON, not a new context/authority owner. @@ -483,7 +458,7 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca return None -def read_replan_view(args: argparse.Namespace) -> dict: +def read_replan_view(args: argparse.Namespace) -> dict[str, Any]: """Read a captured observation only; no registry, provider, guard or receipts.""" from ..control_plane.quota.turn_envelope import _turn_envelope_result from ..command_invocation import current_loopx_cli_argv diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 8d976f5125..3303c2eb3c 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -384,6 +384,7 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r const binding = jsonObject(baseline?.target), prior = jsonObject(baseline?.context); const confirmed = !continuity && retained?.complete === true && equal(retained.target, binding); const assumed = !retained && continuity?.basis === "native_hook_assumption" + && continuity.capture_bytes_verified === true && equal(continuity.target, binding); if (!binding || !prior || (!confirmed && !assumed) || !compatible(binding, target) || binding.context_digest !== digest(prior)) return full("baseline_unavailable"); diff --git a/loopx/control_plane/quota/native_context_lifecycle.ts b/loopx/control_plane/quota/native_context_lifecycle.ts index 0558025cff..17b2da7564 100644 --- a/loopx/control_plane/quota/native_context_lifecycle.ts +++ b/loopx/control_plane/quota/native_context_lifecycle.ts @@ -47,7 +47,8 @@ export function updateNativeContextLifecycle(request: JsonObject): JsonObject { }; if (name === "SessionStart") { const resumable = same && event.source === "resume" && state.context_epoch - && !state.pending && jsonObject(state.candidate)?.basis === NATIVE_CONTEXT_BASIS; + && !state.pending && jsonObject(state.candidate)?.basis === NATIVE_CONTEXT_BASIS + && jsonObject(state.candidate)?.capture_bytes_verified === true; if (!resumable) invalidate(); return state; } @@ -63,11 +64,19 @@ export function updateNativeContextLifecycle(request: JsonObject): JsonObject { const recovery = jsonObject(state.recovery), pending = jsonObject(state.pending); if (!recovery || !jsonObject(recovery.target)) return state; const pages = Array.isArray(request.pages) ? request.pages : []; + // The IO adapter authenticates bodies against journal-pinned capture bytes. + // Self-reported lengths/digests are not that observation. Older callers and + // metadata-only journals must establish a new verified full read. + if (pages.length && request.pages_verified !== true) { + invalidate(); + return state; + } for (const value of pages) { const page = jsonObject(value); if (!page || page.schema_version !== "context_capture_page_v1") continue; const candidate = jsonObject(state.candidate); if (!state.pending && candidate?.capture_sha256 === recovery.sha256 + && candidate?.capture_bytes_verified === true && candidate?.target && page.ok === true) continue; // completed-page replay if (page.ok !== true) { if (pending) invalidate(); @@ -98,7 +107,7 @@ export function updateNativeContextLifecycle(request: JsonObject): JsonObject { state.pending = reads; if (ranges.length !== 1 || ranges[0][0] !== 0 || ranges[0][1] !== total) continue; const target = {...requireJsonObject(recovery.target, "capture target"), context_epoch: state.context_epoch}; - state.candidate = {basis: NATIVE_CONTEXT_BASIS, target, + state.candidate = {basis: NATIVE_CONTEXT_BASIS, capture_bytes_verified: true, target, capture_file: recovery.path, capture_sha256: recovery.sha256, ancestry: original ? 0 : pending?.ancestry ?? 0}; delete state.pending; diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index a8ac68ed44..84e6b79ba7 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -137,10 +137,11 @@ def prepare_context_delivery( def update_native_context_lifecycle(state: Mapping[str, Any], event: Mapping[str, Any], *, - new_epoch: str, pages: list[dict[str, Any]]) -> dict[str, Any]: + new_epoch: str, pages: list[dict[str, Any]], + pages_verified: bool = False) -> dict[str, Any]: """Native IO adapter; the shared typed owner reduces continuity assumptions.""" return _turn_envelope_result("native_context_lifecycle", state=dict(state), - event=dict(event), new_epoch=new_epoch, pages=pages) + event=dict(event), new_epoch=new_epoch, pages=pages, pages_verified=pages_verified) def restore_context_delivery( diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index dabe213a2d..2b8cf48148 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -17,6 +17,7 @@ from .decision_capture import ( write_decision_capture, capture_context_projection, project_captured_context, CONTEXT_CAPTURE_READ_GUIDANCE, + capture_page_range, read_context_capture_file, ) # Native adapter activation reuses scheduler-owned host declarations. These @@ -275,9 +276,11 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str same = state.get("session_id") == session and state.get("cwd") == event.get("cwd") if same: recovery = state.get("recovery") + if name == "PostToolUse": + pages = _verified_context_pages(state, pages) try: updated = update_native_context_lifecycle(state, event, - new_epoch=uuid.uuid4().hex, pages=pages) + new_epoch=uuid.uuid4().hex, pages=pages, pages_verified=True) except (EffectRuntimeRemoteError, EffectRuntimeStartupError): _discard_recovery(root) raise @@ -329,6 +332,34 @@ def _context_pages(value: object) -> list[dict[str, Any]]: return [] +def _verified_context_pages(state: dict[str, Any], pages: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Compare observed output with trusted journal references before reduction. + + Cache each bounded immutable file only within this hook. Neither output + paths nor its self-reported digest can introduce another capture source. + This verifies tool-output bytes, not downstream model-input retention. + """ + references = [value for key in ("recovery", "pending") + if isinstance(value := state.get(key), dict)] + files: dict[str, bytes] = {} + result = [] + for page in pages: + reference = next((value for value in references if value.get("sha256") == page.get("sha256")), None) + if reference is None or page.get("ok") is not True: + result.append(page) # the typed owner handles failures and unrelated reads + continue + try: + digest = reference["sha256"] + if digest not in files: + _, files[digest], _ = read_context_capture_file(reference["path"], digest, private=True) + if capture_page_range(page, raw=files[digest], digest=digest) is None: + raise ValueError("observed context page differs from the immutable capture") + result.append(page) + except (OSError, ValueError, KeyError, TypeError, RecursionError): + return [{**page, "ok": False}] + return result + + def _discard_recovery(root: Path) -> None: """Invalidate only this adapter's journal while its private root is locked.""" path = root / "session.json" @@ -350,13 +381,14 @@ def _native_context_request(state: dict[str, Any], scope: dict[str, Any]) -> dic and not state.get("pending")): try: path = Path(candidate["capture_file"]) - payload = _read(path) + _, _, payload = read_context_capture_file(str(path), candidate["capture_sha256"], private=True) # The shared codec independently validates the complete context # digest and scope. No second read or full native-history scan. baseline = dict(schema_version="context_delivery_v1", mode="full", target=candidate["target"], ancestry=candidate.get("ancestry", 0), context=payload) request.update(baseline=baseline, - continuity={"basis": "native_hook_assumption", "target": candidate["target"]}) + continuity={"basis": "native_hook_assumption", "target": candidate["target"], + "capture_bytes_verified": candidate.get("capture_bytes_verified", False)}) except (OSError, ValueError, KeyError, TypeError, RecursionError): pass # Missing candidate affects reuse, never the current recovery reference. return request @@ -372,7 +404,7 @@ def register_native_context_view(directory: str | None, view: dict[str, Any]) -> if (not isinstance(pending, dict) or pending.get("target") != view["target"] or state.get("session_id") != os.environ.get("CODEX_THREAD_ID")): raise ValueError("native page view does not match the current pending capture") - pending.update(sha256=view["sha256"], total_bytes=view["total_bytes"], + pending.update(path=view["path"], sha256=view["sha256"], total_bytes=view["total_bytes"], ancestry=view.get("ancestry", 0)) _save(root, state) @@ -417,7 +449,7 @@ def project_codex_context(payload: dict[str, Any], *, directory: str | None, cap "total_bytes": Path(recovery["path"]).stat().st_size} # Block reuse before optional wire IO, not after a first page # or EOF. A codec/read interruption keeps this incomplete. - state["pending"] = {"sha256": recovery["sha256"], + state["pending"] = {"path": recovery["path"], "sha256": recovery["sha256"], "total_bytes": state["recovery"]["total_bytes"], "ranges": {}} _save(root, state) recovery_committed = True diff --git a/loopx/control_plane/turn_driver/codex_input_trace.py b/loopx/control_plane/turn_driver/codex_input_trace.py index 4ddb2e1967..4fd5ecfe5c 100644 --- a/loopx/control_plane/turn_driver/codex_input_trace.py +++ b/loopx/control_plane/turn_driver/codex_input_trace.py @@ -10,6 +10,8 @@ from pathlib import Path from typing import Any +from .decision_capture import capture_page_range as _capture_page_range + def read_native_trace_file(path: Path, *, tail: bool = False) -> bytes: from ..effect_runtime import MAX_REQUEST_BYTES @@ -122,24 +124,6 @@ def _read_bundle_payload(bundle: Path, ref: dict[str, Any]) -> dict[str, Any]: return value -def _capture_page_range(page: dict[str, Any], *, raw: bytes, digest: str) -> tuple[int, int] | None: - if page.get("schema_version") != "context_capture_page_v1" or page.get("sha256") != digest: - return None - start, content = page.get("offset"), page.get("content") - if type(start) is not int or start < 0 or not isinstance(content, str) or page.get("ok") is not True: - return None - data = content.encode("utf-8") - end = start + len(data) - if (page.get("total_bytes") != len(raw) or end > len(raw) or not data - or page.get("eof") is not (end == len(raw)) - or page.get("next_offset") != (None if end == len(raw) else end) - or raw[start:end] != data): - # A host preview can remain valid JSON while shortening content - # and retaining the original cursor. It proves no bytes. - return None - return start, end - - def codex_context_in_active_input(observation: dict[str, Any], *, baseline: dict[str, Any], session_id: str) -> bool: """Cross-check a host's retention claim against a native in-flight tool call. diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 8a1440a7a0..7aa5ff9969 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -8,16 +8,17 @@ import socket import stat import tempfile -from collections.abc import Mapping +from collections.abc import Callable, Mapping from pathlib import Path from typing import Any from ..quota.turn_envelope import bind_turn_decision_capture from ..quota.error_codes import QuotaCommandValidationError +from ..content_digest import BARE_SHA256_PATTERN def write_decision_capture( - path: Path, payload: Mapping[str, Any], *, publish=os.link, + path: Path, payload: Mapping[str, Any], *, publish: Callable[[str, Path], None] = os.link, ) -> None: """Publish complete private JSON once; never replace an earlier observation.""" descriptor, temporary = tempfile.mkstemp(prefix=".decision-", dir=path.parent) @@ -123,6 +124,52 @@ def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, pa "--offset", str(offset), "--page-bytes", str(page_bytes)]) +def read_context_capture_file(value: str, digest: str, *, private: bool = False) -> tuple[Path, bytes, dict[str, Any]]: + """One bounded physical-file/hash check for readers and native observations.""" + from ..effect_runtime import MAX_REQUEST_BYTES + + if not isinstance(digest, str) or BARE_SHA256_PATTERN.fullmatch(digest) is None or not value: + raise ValueError("context-read requires the original capture file and its exact SHA-256") + path = Path(value).expanduser().absolute() + # Parent aliases such as macOS /tmp are normal capture locations. + # Resolve those once, but never follow a substituted final file. + path = path.parent.resolve(strict=True) / path.name + if path.is_symlink(): + raise ValueError("context-read requires the physical original capture path, not a symlink") + descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) + with os.fdopen(descriptor, "rb") as stream: + metadata = os.fstat(stream.fileno()) + if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > MAX_REQUEST_BYTES: + raise ValueError("capture must be a regular JSON file within the existing transport size limit") + if private and (metadata.st_uid != os.getuid() or metadata.st_mode & 0o077): + raise ValueError("native context requires a private capture owned by this user") + raw = stream.read(MAX_REQUEST_BYTES + 1) + if len(raw) > MAX_REQUEST_BYTES or hashlib.sha256(raw).hexdigest() != digest: + raise ValueError("capture digest mismatch; restore the original capture, do not rerun admission") + payload = json.loads(raw.decode("utf-8")) + if not isinstance(payload, dict): + raise ValueError("capture must contain a complete JSON object") + return path, raw, payload + + +def capture_page_range(page: dict[str, Any], *, raw: bytes, digest: str) -> tuple[int, int] | None: + """Verify reported page bytes, independently of a host's retention claim.""" + if page.get("schema_version") != "context_capture_page_v1" or page.get("sha256") != digest: + return None + start, content = page.get("offset"), page.get("content") + if type(start) is not int or start < 0 or not isinstance(content, str) or page.get("ok") is not True: + return None + data = content.encode("utf-8") + end = start + len(data) + if (page.get("total_bytes") != len(raw) or end > len(raw) or not data + or page.get("eof") is not (end == len(raw)) + or page.get("next_offset") != (None if end == len(raw) else end) + or raw[start:end] != data): + # Valid page JSON and a correct cursor can still carry the wrong body. + return None + return start, end + + def capture_context_projection(payload: dict[str, Any], *, directory: Path) -> dict[str, Any]: """Save the full projection independently of the optional presentation codec.""" projection = directory / "projection.json" diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 0bbc5bac74..0deeafd718 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -118,12 +118,17 @@ def test_lifecycle_invalidates_and_recovers_current_capture_without_bodies(tmp_p def read_pages(root, wire, *, size=3072): - import argparse - from loopx.cli_commands.quota_capture import native_context_capture_view, read_context_capture + from loopx.cli_commands.quota_capture import native_context_capture_view view = native_context_capture_view(wire, Path(wire["recovery"]["path"]).parent, context_directory=str(root)) - path, digest = view["path"], view["sha256"] + return capture_pages(view["path"], view["sha256"], size=size) + + +def capture_pages(path, digest, *, size=3072): + import argparse + from loopx.cli_commands.quota_capture import read_context_capture + pages = [] offset = 0 while True: @@ -193,6 +198,142 @@ def test_known_incomplete_delivery_cannot_establish_baseline(tmp_path, monkeypat assert json.loads(Path(first["recovery"]["path"]).read_bytes()) == payload +@pytest.mark.parametrize("mode", ["full", "delta"]) +@pytest.mark.parametrize("damage", ["changed_bytes", "missing_file", "changed_file", "symlink"]) +def test_hook_cli_rejects_changed_page_bytes_and_recovers_original(tmp_path, monkeypatch, mode, damage): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, wire = project(tmp_path, payload=payload) + if mode == "delta": + deliver(tmp_path, event, read_pages(tmp_path, wire)) + _, wire = project(tmp_path, "two", payload=payload) + assert wire["mode"] == mode + pages = read_pages(tmp_path, wire, size=8192) + before = context._state(tmp_path) + recovery = before["recovery"] + raw = Path(recovery["path"]).read_bytes() + source = Path(before["pending"]["path"]) + source_raw = source.read_bytes() + changed = [dict(page) for page in pages] + # Keep valid JSON, every byte offset and the self-reported digest unchanged. + # Page coverage and byte length alone cannot distinguish this wrong body. + first = changed[0]["content"] + index = next(i for i, char in enumerate(first) if char.isascii() and char.isalpha()) + if damage == "changed_bytes": + changed[0]["content"] = first[:index] + ("X" if first[index] != "X" else "Y") + first[index + 1:] + assert len(changed[0]["content"].encode()) == len(first.encode()) + assert json.loads("".join(page["content"] for page in changed)) + elif damage == "changed_file": + source.write_bytes(source_raw + b" ") # valid JSON, different pinned digest + else: + source.unlink() + if damage == "symlink": + alias_target = tmp_path / "same-bytes.json" + alias_target.write_bytes(source_raw) + source.symlink_to(alias_target) + repo = Path(__file__).resolve().parents[2] + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + cmd = [sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), "--runtime-root", str(runtime), + "--format", "json", "quota", "context-hook", "--context-delivery", "on", "--codex-app", + "--codex-context-dir", str(tmp_path)] + + def hook(values): + response = {"content": [{"type": "text", "text": json.dumps(page)} for page in values]} + result = subprocess.run(cmd, input=json.dumps({**event, "hook_event_name": "PostToolUse", + "tool_use_id": "capture-read", "tool_response": response}), capture_output=True, text=True, + cwd=tmp_path, env={**os.environ, "PYTHONPATH": str(repo)}, timeout=20) + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == {} + + hook(changed) + assert "candidate" not in context._state(tmp_path) + assert context._state(tmp_path)["recovery"] == recovery + if damage != "changed_bytes": + source.unlink(missing_ok=True) + source.write_bytes(source_raw) + source.chmod(0o600) + assert Path(recovery["path"]).read_bytes() == raw + # Recover the same full capture, including after a damaged delta. Never + # reuse the incomplete wire or collect/admit another business decision. + hook(capture_pages(recovery["path"], recovery["sha256"], size=8192)) + assert context._state(tmp_path)["candidate"]["capture_sha256"] == recovery["sha256"] + _, after = project(tmp_path, "three", payload=payload) + assert after["mode"] == "delta" + assert not registry.exists() and not runtime.exists() + + +def test_legacy_delta_without_pinned_path_recovers_only_from_known_full_capture(tmp_path, monkeypatch): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, full = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, full)) + _, delta = project(tmp_path, "two", payload=payload) + assert delta["mode"] == "delta" + pages = read_pages(tmp_path, delta) + state = context._state(tmp_path) + # An older/incomplete journal has no trusted path for this wire. A path + # supplied by the tool output cannot repair that missing local reference. + path = state["pending"].pop("path") + recovery = state["recovery"] + context._save(tmp_path, state) + deliver(tmp_path, event, [{**page, "path": path} for page in pages]) + assert "candidate" not in context._state(tmp_path) + assert context._state(tmp_path)["recovery"] == recovery + deliver(tmp_path, event, capture_pages(recovery["path"], recovery["sha256"])) + assert context._state(tmp_path)["candidate"]["capture_sha256"] == recovery["sha256"] + + +def test_unverified_page_facts_cannot_promote_through_the_shared_owner(tmp_path, monkeypatch): + from loopx.control_plane.quota.turn_envelope import update_native_context_lifecycle + + event = setup(tmp_path, monkeypatch) + _, wire = project(tmp_path) + pages = read_pages(tmp_path, wire) + before = context._state(tmp_path) + updated = update_native_context_lifecycle(before, {**event, "hook_event_name": "PostToolUse"}, + new_epoch="unverified-pages", pages=pages) + assert "candidate" not in updated and "pending" not in updated + assert updated["recovery"] == before["recovery"] + + +@pytest.mark.parametrize("route", ["resume", "projection"]) +def test_metadata_only_baselines_require_a_new_verified_read(tmp_path, monkeypatch, route): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, wire = project(tmp_path, payload=payload) + pages = read_pages(tmp_path, wire) + deliver(tmp_path, event, pages) + before = context._state(tmp_path) + assert before["candidate"].pop("capture_bytes_verified") is True + context._save(tmp_path, before) + if route == "projection": + _, next_wire = project(tmp_path, "two", payload=payload) + assert next_wire["mode"] == "full" and next_wire["reason"] == "baseline_unavailable" + else: + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + assert "candidate" not in context._state(tmp_path) + assert context._state(tmp_path)["recovery"] == before["recovery"] + deliver(tmp_path, event, pages) + assert context._state(tmp_path)["candidate"]["capture_bytes_verified"] is True + + +@pytest.mark.parametrize("damage", ["whitespace", "public_mode"]) +def test_verified_candidate_requires_unchanged_private_capture(tmp_path, monkeypatch, damage): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, wire = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, wire)) + candidate = context._state(tmp_path)["candidate"] + assert candidate["capture_bytes_verified"] is True + path = Path(candidate["capture_file"]) + if damage == "whitespace": + path.write_bytes(path.read_bytes() + b" ") # identical decoded context, different original bytes + else: + path.chmod(0o644) + _, next_wire = project(tmp_path, "two", payload=payload) + assert next_wire["mode"] == "full" and next_wire["reason"] == "baseline_unavailable" + + def test_parallel_and_duplicate_pages_are_idempotent_and_cannot_borrow_other_scope(tmp_path, monkeypatch): from concurrent.futures import ThreadPoolExecutor @@ -249,7 +390,6 @@ def test_configuration_round_trip_requires_a_new_full_baseline(tmp_path, monkeyp def test_primary_write_failure_preserves_small_recovery_and_does_not_create_ack(tmp_path, monkeypatch): event = setup(tmp_path, monkeypatch) _, wire = project(tmp_path) - before = (tmp_path / "session.json").read_bytes() def unavailable(*args, **kwargs): raise OSError("synthetic journal unavailable") monkeypatch.setattr(context, "_save", unavailable) From 6b0d86002b3824358e4714369bdf6f9bb6ed6a8c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:57:55 +0800 Subject: [PATCH 12/40] fix(context): present native continuity before paged changes Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/quota.py | 7 +---- loopx/cli_commands/quota_capture.py | 20 +++++++++---- loopx/control_plane/quota/context_delivery.ts | 6 +++- .../turn_driver/codex_context.py | 7 +++-- .../turn_driver/decision_capture.py | 21 +++++++++---- .../test_codex_context_capture.py | 3 ++ .../test_codex_context_lifecycle.py | 30 +++++++++++++++++++ .../test_context_delivery_cli.py | 2 ++ 8 files changed, 75 insertions(+), 21 deletions(-) diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index b55c95868d..5dadd7f85d 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -539,15 +539,10 @@ def _emit_quota_result( capture_directory=capture_directory, goal_id=decision_identity[0], agent_id=decision_identity[1], surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet", capture_id=capture_id, continuity_generation=getattr(args, "_context_delivery_generation", None)) - from ..control_plane.turn_driver.decision_capture import CONTEXT_CAPTURE_READ_GUIDANCE - recovery = payload.get("recovery") view = native_context_capture_view(payload, capture_directory, context_directory=directory) payload = {"ok": True, **view, "capture_sha256": capture_id, - **({"recovery": recovery} if recovery else {}), - "instruction": "Presentation of this original decision only; authority and freshness are unchanged. " - + ("This result includes page zero; consume it before next_command. " if "content" in view else "") - + CONTEXT_CAPTURE_READ_GUIDANCE} + **({"recovery": recovery} if recovery else {})} 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 9df68f1e1b..3cae48c410 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -17,7 +17,7 @@ from ..control_plane.effect_runtime import EffectRuntimeRejected 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_read_command, + validate_handoff_socket, CONTEXT_CAPTURE_READ_GUIDANCE, CONTEXT_CAPTURE_PAGE_GUIDANCE, context_capture_read_command, read_context_capture_file as _read_capture, ) @@ -67,7 +67,7 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: "offset": offset, "next_offset": end if end < len(raw) else None, "total_bytes": len(raw), "eof": end == len(raw), "content": raw[offset:end].decode("utf-8")} if context_dir: - result["read_instruction"] = CONTEXT_CAPTURE_READ_GUIDANCE + result["read_instruction"] = CONTEXT_CAPTURE_PAGE_GUIDANCE if end < len(raw): result["next_command"] = context_capture_read_command(path, digest, offset=end, page_bytes=size) return result @@ -106,6 +106,18 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, register_native_context_view(context_directory, {**result, "ancestry": projected.get("ancestry", 0)}) + # 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. + presentation = ("This is the full current capture; after complete reading, continue from it." + if mode == "full" else "Read the current continuity basis and instruction before its change tree.") + result["instruction"] = ( + "Presentation of the original decision only; authority and freshness are unchanged. " + + presentation + " " + + ("This result includes page zero; consume it before next_command. command restarts " + "at zero if the inline page is incomplete or unsupported. " if page["ok"] else "") + + CONTEXT_CAPTURE_PAGE_GUIDANCE + ) return result @@ -195,10 +207,6 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: + CONTEXT_CAPTURE_READ_GUIDANCE} if native_view is not None: result.update(native_view) - if "content" in native_view: - result["instruction"] = ("This result includes page zero. Consume its content, then follow " - "next_command through eof. command restarts at zero if the inline page is incomplete " - "or unsupported. " + result["instruction"]) return result diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 3303c2eb3c..3c04a34607 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -401,7 +401,11 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r + "; all requirements remain binding. A value replaces its subtree; values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; if (bytes(delta) >= bytes(full("delta_not_smaller"))) return full("delta_not_smaller"); if (!equal(applyTree(prior, changes), context)) return full("reconstruction_failed"); - return delta; + // Native consumers page the wire before they can use its baseline. Deliver + // the existing basis and interpretation before the potentially large tree; + // do not duplicate them in a second host instruction or change strict IO. + return assumed ? {schema_version: VERSION, mode: "delta", + continuity_basis: delta.continuity_basis, instruction: delta.instruction, ...delta} : delta; } /** Validate the exact change tree against the immutable CURRENT capture before diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 2b8cf48148..82a107583e 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -16,7 +16,7 @@ import uuid from .decision_capture import ( - write_decision_capture, capture_context_projection, project_captured_context, CONTEXT_CAPTURE_READ_GUIDANCE, + write_decision_capture, capture_context_projection, project_captured_context, CONTEXT_CAPTURE_PAGE_GUIDANCE, capture_page_range, read_context_capture_file, ) @@ -300,7 +300,7 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str return {"hookSpecificOutput": {"hookEventName": name, "additionalContext": "LoopX context baseline invalidated. Before dependent work, read this original full capture. " "Capture recovery does not renew authority or source freshness. " - + CONTEXT_CAPTURE_READ_GUIDANCE + " " + + CONTEXT_CAPTURE_PAGE_GUIDANCE + " " + recovery["command"]}} return {} @@ -443,7 +443,8 @@ def project_codex_context(payload: dict[str, Any], *, directory: str | None, cap if continuity_generation is not None: scope["continuity_generation"] = continuity_generation request = _native_context_request(state, scope) - recovery = capture_context_projection(payload, directory=capture_directory) + recovery = capture_context_projection(payload, directory=capture_directory, + read_guidance=CONTEXT_CAPTURE_PAGE_GUIDANCE) 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 7aa5ff9969..d7d8e74816 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -101,12 +101,13 @@ def validate_handoff_socket(value: str) -> Path: return path -CONTEXT_CAPTURE_READ_GUIDANCE = ( +_CONTEXT_CAPTURE_PAGE_START = ( "Follow next_command through eof and combine content in byte-offset order. " "In code-mode, forward successful exec stdout directly with text(result.output); " "retain errors. The reader verifies the original capture SHA-256; keep its returned digest and byte cursors. " - "Do not hash or retype the packet. Native input observation independently verifies complete model-visible " - "bytes before delta reuse; neither a retyped hash nor a disk hash proves that input. " + "Do not hash or retype the packet. " +) +_CONTEXT_CAPTURE_PAGE_END = ( "Small batches can save model round trips; start with at most two default pages " "and inspect every output. A host may truncate inside valid JSON or omit a whole output. " "Consume all model-visible content in UTF-8 byte-offset order with complete coverage through total_bytes. " @@ -114,6 +115,15 @@ def validate_handoff_socket(value: str) -> Path: "of this same capture with fewer pages or a smaller --page-bytes; do not trust a preview or EOF alone. " "Do not rerun admission or ask the owner. File existence and page EOF are not retention proof." ) +# Native paging verifies capture bytes; the existing codec separately declares +# its continuity basis. Keep the strict caller's original guidance unchanged. +CONTEXT_CAPTURE_PAGE_GUIDANCE = _CONTEXT_CAPTURE_PAGE_START + _CONTEXT_CAPTURE_PAGE_END +CONTEXT_CAPTURE_READ_GUIDANCE = ( + _CONTEXT_CAPTURE_PAGE_START + + "Native input observation independently verifies complete model-visible " + "bytes before delta reuse; neither a retyped hash nor a disk hash proves that input. " + + _CONTEXT_CAPTURE_PAGE_END +) def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, page_bytes: int = 4096) -> str: @@ -170,7 +180,8 @@ def capture_page_range(page: dict[str, Any], *, raw: bytes, digest: str) -> tupl return start, end -def capture_context_projection(payload: dict[str, Any], *, directory: Path) -> dict[str, Any]: +def capture_context_projection(payload: dict[str, Any], *, directory: Path, + read_guidance: str = CONTEXT_CAPTURE_READ_GUIDANCE) -> dict[str, Any]: """Save the full projection independently of the optional presentation codec.""" projection = directory / "projection.json" write_decision_capture(projection, payload) @@ -179,7 +190,7 @@ def capture_context_projection(payload: dict[str, Any], *, directory: Path) -> d "path": str(projection), "sha256": digest, "instruction": "Complete JSON for this original capture. If the baseline is missing, compacted, " "truncated or uncertain, use this full projection before dependent work. " - + CONTEXT_CAPTURE_READ_GUIDANCE, + + read_guidance, "command": context_capture_read_command(projection, digest), } diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index dd3e8cf9c8..70b356ef66 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -248,6 +248,9 @@ def response(request, number, message): wire_file = Path(descriptor["path"]).parent / "delivery.json" wires[name] = json.loads(wire_file.read_text()) assert descriptor["mode"] == wires[name]["mode"] == ("full" if name == "cold" else "delta") + assert "Native input observation independently verifies" not in descriptor["instruction"] + if name == "warm": + assert json.dumps(wires[name]["instruction"]).encode() in descriptor["content"].encode() capture = Path(wires[name]["recovery"]["path"]).parent / "decision.json" snapshots.append((capture, capture.read_bytes())) read_paths[name] = Path(wires[name]["recovery"]["path"]) if name == "cold" else wire_file diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 0deeafd718..02d6945d0f 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -171,6 +171,36 @@ def test_complete_reader_delivery_allows_delta_without_diagnostics_or_ack(tmp_pa assert third["mode"] == "delta" +def test_native_page_zero_exposes_current_continuity_and_preserves_full_recovery(tmp_path, monkeypatch): + from argparse import Namespace + from loopx.cli_commands.quota_capture import native_context_capture_view, read_context_capture + + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Keep all requirements. " * 1000}, + "current_control": {"status": "Current controls remain inline. " * 240}} + _, first = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, first)) + _, second = project(tmp_path, "two", payload=payload) + assert second["mode"] == "delta" and second["continuity_basis"] == "native_hook_assumption" + view = native_context_capture_view(second, tmp_path / "two", context_directory=str(tmp_path)) + # The consumer must see the codec's actual basis before a long change tree, + # rather than a second transport layer claiming strict final-input proof. + assert json.dumps(second["instruction"]).encode() in view["content"].encode() + assert b'"continuity_basis":"native_hook_assumption"' in view["content"].encode() + for instruction in (view["instruction"], second["recovery"]["instruction"]): + assert "Native input observation independently verifies" not in instruction + deliver(tmp_path, event, capture_pages(view["path"], view["sha256"])) + before = (tmp_path / "session.json").read_bytes() + # Explicit loss recovery stays available even after a complete delta. Do + # not turn the engineering assumption into a denial of a full original read. + page = read_context_capture(Namespace(codex_context_dir=str(tmp_path), + goal_id="fixture", agent_id="agent", offset=0, page_bytes=3072)) + assert page["ok"] and page["sha256"] == second["recovery"]["sha256"] + pages = capture_pages(second["recovery"]["path"], page["sha256"]) + assert json.loads("".join(p["content"] for p in pages)) == payload + assert (tmp_path / "session.json").read_bytes() == before + + @pytest.mark.parametrize("failure", ["tail_only", "gap", "truncated", "interrupted", "reader_error", "failed_tool"]) def test_known_incomplete_delivery_cannot_establish_baseline(tmp_path, monkeypatch, failure): event = setup(tmp_path, monkeypatch) diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 258a27aacc..26e837b24c 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -249,6 +249,8 @@ def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(t "--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 From 0e86cac2f451027b8db42907e629d9cc39fa4042 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:56:22 +0800 Subject: [PATCH 13/40] fix(context): recover original capture before native resume reuse Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 14 +++++--- .../quota/native_context_lifecycle.ts | 8 ++--- .../test_codex_context_capture.py | 14 +++++++- .../test_codex_context_lifecycle.py | 33 ++++++++++++++++++- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index feca8e5d68..5789051649 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -243,9 +243,11 @@ history. Benchmark preparation stages those same hooks in its isolated trial hom `PostToolUse` records complete digest-bound reader ranges in the existing session journal; EOF without preceding pages, a known malformed/truncated page or a reader error cannot establish a baseline. Concurrent and repeated pages coalesce. -`PreCompact`, `PostCompact`, clear, fork, a changed session/workspace and an -unresumable restore invalidate it. Same-session resume preserves a complete -baseline; ordinary tools and turn endings do not invalidate it. An interrupted +`PreCompact`, `PostCompact`, clear, fork, a changed session/workspace and resume +invalidate it. A resume may supply different history under the same session ID; +the [native event contract](https://learn.chatgpt.com/docs/hooks#sessionstart) +does not attest retention. Read the pinned original completely before establishing +a new differential chain. Ordinary tools and turn endings do not invalidate it. An interrupted pending delivery requires full recovery. Repeated reset events are idempotent. Recovery reads the same immutable capture without re-admission or spend. @@ -382,8 +384,10 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 `PostToolUse` 在现有 session journal 中合并摘要固定的完整读取范围;缺少前页的 EOF、 已知的格式损坏/截断或读取失败不能建立基线。并发、重复页幂等合并。`PreCompact`、 -`PostCompact`、清空、分叉、会话/工作区切换及无法接续的恢复使基线失效;同会话的完整 -基线可在 resume 后继续使用,普通工具和正常轮次结束不无条件失效。尚未完成的交付被中断 +`PostCompact`、清空、分叉、会话/工作区切换及 resume 使基线失效;同一会话 ID 的 resume +也可能使用不同历史,[原生事件契约](https://learn.chatgpt.com/docs/hooks#sessionstart)本身不证明保留。 +须完整读取固定的原捕获后再建立差分链。 +普通工具和正常轮次结束不无条件失效。尚未完成的交付被中断 后须完整恢复。重复重置事件幂等;恢复读取同一不可变捕获,不重新准入或扣额。 这是以生命周期假设替换逐轮完整输入证明。Hook 未暴露的下游丢失/截断、漏掉失效事件, diff --git a/loopx/control_plane/quota/native_context_lifecycle.ts b/loopx/control_plane/quota/native_context_lifecycle.ts index 17b2da7564..275a47f14d 100644 --- a/loopx/control_plane/quota/native_context_lifecycle.ts +++ b/loopx/control_plane/quota/native_context_lifecycle.ts @@ -46,10 +46,10 @@ export function updateNativeContextLifecycle(request: JsonObject): JsonObject { delete state.active_call; // retire legacy diagnostic-call binding }; if (name === "SessionStart") { - const resumable = same && event.source === "resume" && state.context_epoch - && !state.pending && jsonObject(state.candidate)?.basis === NATIVE_CONTEXT_BASIS - && jsonObject(state.candidate)?.capture_bytes_verified === true; - if (!resumable) invalidate(); + // A resume may supply different history under the same session id. The + // event carries no retained-context proof, so recover the pinned original + // before establishing a new chain; ordinary tools/turn endings stay warm. + invalidate(); return state; } if (name === "PreCompact" || name === "PostCompact") { diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 70b356ef66..fc5a69ecd2 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -239,7 +239,19 @@ def response(request, number, message): stages["phase"] = name + "_ready" if phase == "resuming": assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") - cmd = invoke(name) + assert "LoopX context baseline invalidated." in _input_text(request) + assert wires["cold"]["recovery"]["command"] in _input_text(request) + # Resume itself does not attest history retention. Read the + # same original through the real CLI before the next guard. + stages["phase"] = "resume_read" + stages["pages"] = pages("cold", read_paths["cold"]) + cmd = stages["pages"].pop(0) + else: + cmd = invoke(name) + elif phase == "resume_read": + assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") + stages["phase"] = "warm_ready" + cmd = invoke("warm") elif phase in {"cold_ready", "warm_ready"}: name = phase.split("_")[0] descriptor = json.loads(wire_paths[name].read_text()) diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 02d6945d0f..99f3671ed0 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -157,7 +157,6 @@ def test_complete_reader_delivery_allows_delta_without_diagnostics_or_ack(tmp_pa state = context._state(tmp_path) assert state["candidate"]["basis"] == "native_hook_assumption" and "pending" not in state assert "retained" not in state and "active_call" not in state - assert context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) == {} # Ordinary tools and normal turn endings preserve the completed baseline. for name in ("PreToolUse", "PostToolUse", "Stop", "SessionEnd"): context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": name, @@ -201,6 +200,38 @@ def test_native_page_zero_exposes_current_continuity_and_preserves_full_recovery assert (tmp_path / "session.json").read_bytes() == before +@pytest.mark.parametrize("mode", ["full", "delta"]) +def test_resume_requires_complete_original_read_before_reusing_baseline(tmp_path, monkeypatch, mode): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, wire = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, wire)) + if mode == "delta": + _, wire = project(tmp_path, "two", payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, wire)) + before = context._state(tmp_path) + recovery = before["recovery"] + original = Path(recovery["path"]).read_bytes() + resumed = {**event, "source": "resume"} + result = context.codex_context_hook(str(tmp_path), resumed) + after = context._state(tmp_path) + assert "candidate" not in after and after["context_epoch"] != before["context_epoch"] + assert after["recovery"] == recovery + assert recovery["command"] in result["hookSpecificOutput"]["additionalContext"] + # Repeated notifications do not rotate the epoch or replace the recovery. + context.codex_context_hook(str(tmp_path), resumed) + assert context._state(tmp_path) == after + pages = capture_pages(recovery["path"], recovery["sha256"]) + assert len(pages) > 1 + deliver(tmp_path, event, pages[:1]) + assert "candidate" not in context._state(tmp_path) + deliver(tmp_path, event, pages[1:]) + assert Path(recovery["path"]).read_bytes() == original + assert context._state(tmp_path)["candidate"]["target"]["context_epoch"] == after["context_epoch"] + _, next_wire = project(tmp_path, "post-resume", payload=payload) + assert next_wire["mode"] == "delta" + + @pytest.mark.parametrize("failure", ["tail_only", "gap", "truncated", "interrupted", "reader_error", "failed_tool"]) def test_known_incomplete_delivery_cannot_establish_baseline(tmp_path, monkeypatch, failure): event = setup(tmp_path, monkeypatch) From 25a9fd826f4eceaa6d5c596df6aff2d49b177ee3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:00:55 +0800 Subject: [PATCH 14/40] fix(checkpoint): preserve omitted native context mode as off Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/tests/test_native_checkpoint.py | 15 +++++++++++++++ loopx/hosts/codex_checkpoint.py | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/benchmark/tests/test_native_checkpoint.py b/benchmark/tests/test_native_checkpoint.py index b93d3ca24e..311d9699f6 100644 --- a/benchmark/tests/test_native_checkpoint.py +++ b/benchmark/tests/test_native_checkpoint.py @@ -930,6 +930,21 @@ def test_interrupted_cursor_requires_exact_unreleased_final_guard(tmp_path, monk rendered[-1]["items"][-1]["command"] = shlex.join(["/bin/bash", "-lc", "LOOPX_TURN=turn-1\n" + shlex.join(argv).replace("--turn-instance-id turn-1", '--turn-instance-id "${LOOPX_TURN:?}"')]) _verify_interrupted_guard(rendered, guard) + for setting in ("off", "on"): + explicit = copy.deepcopy(turns) + explicit[-1]["items"][-1]["command"] = shlex.join([ + "/bin/bash", "-lc", shlex.join(argv + ["--context-delivery", setting])]) + _verify_interrupted_guard(explicit, {**guard, "context_delivery": setting}) + with pytest.raises(ValueError, match="routes or identity"): + _verify_interrupted_guard(turns, {**guard, "context_delivery": "on"}) + with pytest.raises(ValueError, match="routes or identity"): + _verify_interrupted_guard(turns, {**guard, "context_delivery": None}) + # Equivalent implicit/explicit off commands cannot hide an earlier call. + repeated = copy.deepcopy(turns) + repeated[-1]["items"].append({**item, "id": "later-call", "command": shlex.join([ + "/bin/bash", "-lc", shlex.join(argv + ["--context-delivery", "off"])])}) + with pytest.raises(ValueError, match="ambiguous"): + _verify_interrupted_guard(repeated, guard) # A caller cannot change the captured presentation/authority mode on restore. opposite = list(argv) if "--turn-envelope" in opposite: diff --git a/loopx/hosts/codex_checkpoint.py b/loopx/hosts/codex_checkpoint.py index 5a6587aa8d..95fcbd2253 100644 --- a/loopx/hosts/codex_checkpoint.py +++ b/loopx/hosts/codex_checkpoint.py @@ -102,7 +102,12 @@ def arguments(command): argv = _guard_cli_arguments(command, guard) try: with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): - return vars(parser.parse_args(argv)) + parsed = vars(parser.parse_args(argv)) + # A sealed command is independent of today's machine preference. + # Legacy omitted transport flags mean off; explicit on stays distinct. + if parsed.get("context_delivery") is None: + parsed["context_delivery"] = "off" + return parsed except (SystemExit, ValueError) as error: raise ValueError("native pending guard arguments are invalid") from error args = arguments(item.get("command", "")) From 03655877efbfc492dd1085505449d12bb7af0e6b Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:19:48 +0800 Subject: [PATCH 15/40] test(context): qualify capture reader digest ownership Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_content_digest_single_owner.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/architecture/test_content_digest_single_owner.py b/tests/architecture/test_content_digest_single_owner.py index c5276c364d..d71af03090 100644 --- a/tests/architecture/test_content_digest_single_owner.py +++ b/tests/architecture/test_content_digest_single_owner.py @@ -204,6 +204,7 @@ "loopx.control_plane.todos.completion_validation_store", "loopx.control_plane.todos.machine_section_projection", "loopx.control_plane.turn_driver.codex_sessions", + "loopx.control_plane.turn_driver.decision_capture", "loopx.control_plane.turn_driver.operation_requests", "loopx.control_plane.work_items.governed_transition_proposal", "loopx.control_plane.work_items.progress_review_policy", @@ -1329,6 +1330,27 @@ def test_the_owner_is_a_leaf_and_exports_only_the_two_shapes() -> None: assert patterns == set(CANONICAL_EXPORTS), patterns +def test_context_capture_reader_checks_bare_shape_and_original_bytes(tmp_path: Path) -> None: + from hashlib import sha256 + + from loopx.control_plane.turn_driver.decision_capture import read_context_capture_file + + raw = b'{"ok":true}\n' + capture = tmp_path / "capture.json" + capture.write_bytes(raw) + digest = sha256(raw).hexdigest() + path, received, payload = read_context_capture_file(str(capture), digest) + assert path == capture.resolve() + assert received == raw and payload == {"ok": True} + + for invalid in (*BARE_REJECTS, f"sha256:{digest}", digest.upper(), f"{digest}\n"): + with pytest.raises(ValueError, match="exact SHA-256"): + read_context_capture_file(str(capture), invalid) + for wrong_bytes in BARE_ACCEPTS: + with pytest.raises(ValueError, match="capture digest mismatch"): + read_context_capture_file(str(capture), wrong_bytes) + + def test_periodic_report_generation_receipt_checks_both_digest_fields() -> None: """The site a reviewer named: two digest fields, checked by one reader. From 91a68d9f3b7af7441d2f2e462775a046480daaf5 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:58:27 +0800 Subject: [PATCH 16/40] fix(context): retire native baseline after journal failures Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/heartbeat-automation-prompt.md | 7 +- docs/reference/required-work-context.md | 13 +++- .../turn_driver/codex_context.py | 30 ++++----- .../test_codex_context_lifecycle.py | 66 +++++++++++++++++++ 4 files changed, 94 insertions(+), 22 deletions(-) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 571fb745e6..085dad3812 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -485,9 +485,10 @@ Native reuse assumes a normally completed full read was read and remains available until a lifecycle invalidation. `PostToolUse` merges complete reader ranges in the existing private session journal. EOF alone, known read failures, malformed/truncated pages and interrupted paging cannot establish a baseline. -Compaction, clear, fork, session/workspace change and an unresumable restore -invalidate it; ordinary tools and turn endings do not. Same-session resume can -reuse a completed baseline. Repeated events and parallel pages are idempotent. +Compaction, clear, fork, session/workspace change and every SessionStart, including +same-session resume, invalidate it; ordinary tools and turn endings do not. +Read the pinned original completely before establishing a new differential chain. +Repeated events and parallel pages are idempotent. This replaces per-request input-history proof, not the capture integrity check. It preserves native providers, authentication, endpoints and connection mode, including implicit history. No input diagnostics or forced HTTP is required. diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 5789051649..af4e2b1d14 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -255,8 +255,12 @@ This deliberately replaces per-request complete-input proof with lifecycle assumptions. Downstream output loss or truncation not exposed to the hook, a missed invalidation, or hooks silently disabled outside the declared setting may escape detection. These limits are not certified retention and do not add an -approval gate. Explicit off keeps ordinary full delivery; known journal failures -remove the reusable baseline and retain the original explicit recovery reference. +approval gate. Explicit off keeps ordinary full delivery. A journal read, lifecycle +reduction or write failure after acquiring its lock retires that journal before +transient IO recovery can revive the old baseline. The immutable capture and its +already-delivered explicit recovery reference remain available; if the journal +could not be read, session lookup is unavailable. Failure to reach or retire the +journal at all remains part of the missed-hook limitation above. Generated machine-inherited hooks reload the same machine runtime, so off or removal stops their journal writes. Explicit manual on options remain explicit. Replacing the machine configuration invalidates reuse, including off/on round @@ -392,7 +396,10 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 这是以生命周期假设替换逐轮完整输入证明。Hook 未暴露的下游丢失/截断、漏掉失效事件, 或在声明开关之外静默禁用 hooks,仍可能漏检;不把这些假设称为实测保留,也不新增审批门。 -显式关闭继续普通全量投递;已知 journal 失败移除可复用基线,保留原始显式恢复引用。 +显式关闭继续普通全量投递。持锁后的 journal 读取、生命周期归约或写入失败会移除该 +journal,防止短暂 IO 故障恢复后复活旧基线。不可变 capture 与先前交付的显式恢复引用 +仍可读取;无法读取 journal 时,会话查找不可用。完全无法访问或移除 journal 的情况 +仍属于上述 hook 漏报限制。 由机器继承生成的 hook 每次重读同一个 runtime,关闭或移除 配置后停止 journal 写入;手工显式 on 保留其显式优先级。 机器配置被替换后重新全量,包括最终内容相同的 off/on 往返;其他机器配置编辑也可能 diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 82a107583e..29e10b836b 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -272,27 +272,25 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str recovery = None try: with _locked(directory) as root: - state = _state(root) - same = state.get("session_id") == session and state.get("cwd") == event.get("cwd") - if same: - recovery = state.get("recovery") - if name == "PostToolUse": - pages = _verified_context_pages(state, pages) try: + state = _state(root) + same = state.get("session_id") == session and state.get("cwd") == event.get("cwd") + if same: + recovery = state.get("recovery") + if name == "PostToolUse": + pages = _verified_context_pages(state, pages) updated = update_native_context_lifecycle(state, event, new_epoch=uuid.uuid4().hex, pages=pages, pages_verified=True) - except (EffectRuntimeRemoteError, EffectRuntimeStartupError): + if updated != state: + _save(root, updated) + recovery_needed = same and not updated.get("candidate") + except (OSError, ValueError, TypeError, RecursionError, EffectRuntimeRemoteError, EffectRuntimeStartupError): + # A transient read/reduction failure can miss invalidation just + # as a failed write can. While holding the same lock, retire + # this journal before IO recovery can revive its old candidate. + # Immutable full captures and explicit references stay intact. _discard_recovery(root) raise - if updated != state: - try: - _save(root, updated) - except (OSError, ValueError): - # The explicit recovery reference remains readable, but a - # failed invalidation must not leave the old candidate live. - _discard_recovery(root) - raise - recovery_needed = same and not updated.get("candidate") except (OSError, ValueError, TypeError, RecursionError, EffectRuntimeRemoteError, EffectRuntimeStartupError): recovery_needed = True if (name == "SessionStart" and recovery_needed diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 99f3671ed0..220b4f4e4e 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -460,6 +460,48 @@ def unavailable(*args, **kwargs): assert Path(wire["recovery"]["path"]).exists() +@pytest.mark.parametrize("event_name", ["SessionStart", "PreCompact", "PostCompact"]) +@pytest.mark.parametrize("failure", ["read", "reduction"]) +def test_hook_failure_under_lock_cannot_resurrect_a_completed_baseline(tmp_path, monkeypatch, event_name, failure): + from loopx.cli_commands.quota_capture import read_context_capture + from argparse import Namespace + + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, first = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, first)) + assert context._state(tmp_path)["candidate"]["capture_bytes_verified"] is True + recovery = first["recovery"] + original = Path(recovery["path"]).read_bytes() + + def unavailable(*args, **kwargs): + raise OSError("synthetic journal read unavailable") if failure == "read" else ValueError("invalid lifecycle result") + + with monkeypatch.context() as failed: + if failure == "read": + failed.setattr(context, "_state", unavailable) + else: + failed.setattr("loopx.control_plane.quota.turn_envelope.update_native_context_lifecycle", unavailable) + result = context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": event_name, "source": "compact"}) + if event_name == "SessionStart" and failure == "reduction": + assert recovery["command"] in result["hookSpecificOutput"]["additionalContext"] + else: + assert result == {} + + # The read/reducer can recover on the very next call. That must not revive + # the candidate that missed this known invalidation. The immutable original + # remains available through the already-delivered explicit reference. + page = read_context_capture(Namespace(capture_file=recovery["path"], capture_sha256=recovery["sha256"], + offset=0, page_bytes=8192)) + assert page["ok"] and original.startswith(page["content"].encode()) + assert Path(recovery["path"]).read_bytes() == original + _, fallback = project(tmp_path, "after-failure", payload=payload) + assert fallback.get("mode", "full") == "full" and fallback.get("context", fallback) == payload + context.codex_context_hook(str(tmp_path), event) + _, next_wire = project(tmp_path, "after-restart", payload=payload) + assert next_wire["mode"] == "full" + + def _assert_recovery_after_fallback(root, event, source, capture): import argparse import hashlib @@ -634,6 +676,30 @@ def test_real_hook_cli_is_transport_only_without_registry_or_installation(tmp_pa assert not registry.exists() and not runtime.exists() +def test_real_hook_cli_retires_unreadable_journal_before_io_recovers(tmp_path, monkeypatch): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, first = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, first)) + journal = tmp_path / "session.json" + original = Path(first["recovery"]["path"]).read_bytes() + journal.chmod(0o644) # The real reader must reject a non-private journal. + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + installed_cli = os.environ.get("LOOPX_NATIVE_CONTEXT_CLI") + cli = [installed_cli] if installed_cli else [sys.executable, "-m", "loopx.entrypoint"] + result = subprocess.run([*cli, "--registry", str(registry), + "--runtime-root", str(runtime), "quota", "context-hook", "--context-delivery", "on", "--codex-app", + "--codex-context-dir", str(tmp_path)], input=json.dumps({**event, "hook_event_name": "PreCompact"}), + capture_output=True, text=True, timeout=20, cwd=Path(__file__).resolve().parents[2]) + assert result.returncode == 0 and json.loads(result.stdout) == {}, result.stderr + if journal.exists(): + journal.chmod(0o600) # Simulate IO recovery; never rewrite the candidate. + _, fallback = project(tmp_path, "after-cli-failure", payload=payload) + assert fallback.get("mode", "full") == "full" and fallback.get("context", fallback) == payload + assert Path(first["recovery"]["path"]).read_bytes() == original + assert not registry.exists() and not runtime.exists() + + @pytest.mark.parametrize("damage", [None, "session", "workspace", "goal", "agent", "missing_scope", "missing", "corrupt", "public", "symlink"]) def test_lost_hook_reference_is_scoped_read_only_and_pins_later_pages(tmp_path, monkeypatch, damage): import argparse From a19a87c179601b02f4c435161c3c42f74e8c0426 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:26:07 +0800 Subject: [PATCH 17/40] fix(context): avoid old-capture prefetch for new decisions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 12 ++- .../quota/native_context_lifecycle.ts | 5 +- .../turn_driver/codex_context.py | 26 +++++-- .../test_codex_context_lifecycle.py | 36 +++++++++ .../test_context_delivery_cli.py | 77 +++++++++++++++++++ 5 files changed, 143 insertions(+), 13 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index af4e2b1d14..07b6df27c5 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -246,8 +246,12 @@ error cannot establish a baseline. Concurrent and repeated pages coalesce. `PreCompact`, `PostCompact`, clear, fork, a changed session/workspace and resume invalidate it. A resume may supply different history under the same session ID; the [native event contract](https://learn.chatgpt.com/docs/hooks#sessionstart) -does not attest retention. Read the pinned original completely before establishing -a new differential chain. Ordinary tools and turn endings do not invalidate it. An interrupted +does not attest retention. A new independent decision uses its normal guarded +context command and reads the resulting current full capture; it need not first +read an older capture to seed a delta. Continuing a captured decision after context +loss still requires complete recovery of that exact original, never a new guard +as a substitute. Both paths establish a new chain only after a complete verified +full read. Ordinary tools and turn endings do not invalidate it. An interrupted pending delivery requires full recovery. Repeated reset events are idempotent. Recovery reads the same immutable capture without re-admission or spend. @@ -390,7 +394,9 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 已知的格式损坏/截断或读取失败不能建立基线。并发、重复页幂等合并。`PreCompact`、 `PostCompact`、清空、分叉、会话/工作区切换及 resume 使基线失效;同一会话 ID 的 resume 也可能使用不同历史,[原生事件契约](https://learn.chatgpt.com/docs/hooks#sessionstart)本身不证明保留。 -须完整读取固定的原捕获后再建立差分链。 +新的独立决定走正常准入与上下文命令,完整读取其返回的当前全量捕获;不必为了建立差分 +基线而先读旧捕获。若丢失上下文后继续某个已捕获决定的依赖工作,仍须完整恢复那份精确 +原捕获,不能用新 guard 替代。两条路径都只在完整且已校验的全量读取后建立新差分链。 普通工具和正常轮次结束不无条件失效。尚未完成的交付被中断 后须完整恢复。重复重置事件幂等;恢复读取同一不可变捕获,不重新准入或扣额。 diff --git a/loopx/control_plane/quota/native_context_lifecycle.ts b/loopx/control_plane/quota/native_context_lifecycle.ts index 275a47f14d..863b2797ec 100644 --- a/loopx/control_plane/quota/native_context_lifecycle.ts +++ b/loopx/control_plane/quota/native_context_lifecycle.ts @@ -47,8 +47,9 @@ export function updateNativeContextLifecycle(request: JsonObject): JsonObject { }; if (name === "SessionStart") { // A resume may supply different history under the same session id. The - // event carries no retained-context proof, so recover the pinned original - // before establishing a new chain; ordinary tools/turn endings stay warm. + // event carries no retained-context proof. A fresh decision starts with a + // current full capture; dependent continuation recovers its pinned original + // before establishing a new chain. Ordinary tools/turn endings stay warm. invalidate(); return state; } diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 29e10b836b..58ce5125e3 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -198,6 +198,20 @@ def read_codex_context_reference(directory: str, *, goal_id: str | None, agent_i return path, digest +def _context_continuation_instructions(command: str) -> str: + """Distinguish fresh full delivery from recovery of a dependent decision.""" + return ( + "For a new independent decision, run its normal guarded context command first. " + "Without a usable baseline it returns the current full capture; read it completely before dependent work. " + "Do not read an older capture merely to seed a delta. " + "If continuing a captured LoopX decision after context loss (including compaction or a failed hook), " + "first recover that exact original capture with: " + command + ". " + "A fresh guard cannot replace recovery of that decision. " + "If recovery fails, restore the original capture/reference; do not guess or ask the owner. " + "Recovery never renews authority/freshness or proves retention. Independent authorized work can continue. " + ) + + def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_id: str, cli_argv: list[str], session_root: bool = False) -> str: """Host-owned persistent instructions, independent of event-hook delivery.""" @@ -207,11 +221,8 @@ def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_i "--codex-context-root" if session_root else "--codex-context-dir", directory, "--goal-id", goal_id, "--agent-id", agent_id]) return ( - "If continuing a captured LoopX decision and its complete context or recovery reference was lost " - "(including compaction or a failed hook), recover before dependent work with: " + command + ". " + _context_continuation_instructions(command) + "Follow the returned next_command through eof; verify complete bytes and digest. " - "If lookup fails, restore the original capture/reference; do not guess, readmit or ask the owner. " - "Recovery never renews authority/freshness or proves retention. Independent authorized work can continue. " "For code-mode reads, size batches against both direct-result and retained-history output limits; " "max_output_tokens alone does not establish the latter. Start with at most two default pages when " "the effective history limit is unknown. With a known sufficient limit, larger batches can reduce " @@ -296,10 +307,9 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str if (name == "SessionStart" and recovery_needed and isinstance(recovery, dict) and recovery.get("command")): return {"hookSpecificOutput": {"hookEventName": name, "additionalContext": - "LoopX context baseline invalidated. Before dependent work, read this original full capture. " - "Capture recovery does not renew authority or source freshness. " - + CONTEXT_CAPTURE_PAGE_GUIDANCE + " " - + recovery["command"]}} + "LoopX context baseline invalidated. " + + _context_continuation_instructions(recovery["command"]) + + CONTEXT_CAPTURE_PAGE_GUIDANCE}} return {} diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 220b4f4e4e..3936c84ff2 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -232,6 +232,42 @@ def test_resume_requires_complete_original_read_before_reusing_baseline(tmp_path assert next_wire["mode"] == "delta" +@pytest.mark.parametrize("mode", ["full", "delta"]) +def test_fresh_capture_after_resume_does_not_require_reading_old_original(tmp_path, monkeypatch, mode): + event = setup(tmp_path, monkeypatch) + payload = {"ok": True, "plan_summary": {"requirements": "Preserve every obligation. " * 1000}} + _, wire = project(tmp_path, payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, wire)) + if mode == "delta": + _, wire = project(tmp_path, "two", payload=payload) + deliver(tmp_path, event, read_pages(tmp_path, wire)) + original = wire["recovery"] + before = Path(original["path"]).read_bytes() + resumed = context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + guidance = resumed["hookSpecificOutput"]["additionalContext"] + assert guidance.index("new independent decision") < guidance.index(original["command"]) + assert "A fresh guard cannot replace recovery" in guidance + assert "candidate" not in context._state(tmp_path) + + # A fresh authoritative input must be full. No old reader pages are + # delivered to seed this chain, and the changed requirement remains whole. + current = {**payload, "current_requirement": "Stop before publishing this new result."} + _, fresh = project(tmp_path, "fresh", payload=current) + assert fresh["mode"] == "full" and fresh["context"] == current + pages = read_pages(tmp_path, fresh) + deliver(tmp_path, event, pages[:1]) + assert "candidate" not in context._state(tmp_path) + deliver(tmp_path, event, pages[1:]) + candidate = context._state(tmp_path)["candidate"] + assert candidate["target"] == fresh["target"] + assert candidate["capture_sha256"] == fresh["recovery"]["sha256"] + _, next_wire = project(tmp_path, "after-fresh", payload=current) + assert next_wire["mode"] == "delta" and next_wire["base"] == fresh["target"] + assert Path(original["path"]).read_bytes() == before + assert json.loads("".join(p["content"] for p in capture_pages( + original["path"], original["sha256"]))) == payload + + @pytest.mark.parametrize("failure", ["tail_only", "gap", "truncated", "interrupted", "reader_error", "failed_tool"]) def test_known_incomplete_delivery_cannot_establish_baseline(tmp_path, monkeypatch, failure): event = setup(tmp_path, monkeypatch) diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 26e837b24c..76bb8924ee 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -272,6 +272,83 @@ def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(t assert _spend_run_count(runtime) == 0 and state.read_bytes() == original +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_native_fresh_work_turn_reads_current_full_without_old_recovery(tmp_path, monkeypatch, provider): + import shlex + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import read_context_capture + from loopx.control_plane.turn_driver import codex_context as context + + isolate_sqlite_runtime(tmp_path, monkeypatch) + monkeypatch.setenv("CODEX_THREAD_ID", "native-fresh-session") + project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + todos = read_fixture_todos(state) + todos[0]["text"] = "Retain every original obligation. " * 500 + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim"), + state_path=state, provider=provider) + root, captures = tmp_path / "context", tmp_path / "captures" + root.mkdir(mode=0o700) + captures.mkdir() + event = dict(session_id="native-fresh-session", cwd=str(project), + hook_event_name="SessionStart", source="startup") + context.codex_context_hook(str(root), event) + + def guard(turn): + rc, view = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, + "--turn-instance-id", turn, "--decision-output-root", str(captures), + "--context-delivery", "on", "--codex-context-dir", str(root), cwd=project) + assert rc == 0, view + return view + + def consume(view): + page, observed = view, b"" + while True: + assert page["offset"] == len(observed) + observed += page["content"].encode() + context.codex_context_hook(str(root), {**event, "hook_event_name": "PostToolUse", + "tool_response": json.dumps(page)}) + if page["eof"]: + break + page = read_context_capture(build_parser().parse_args(shlex.split(page["next_command"])[1:])) + assert hashlib.sha256(observed).hexdigest() == view["sha256"] + return json.loads(observed) + + first = guard("native-work-first") + assert first["mode"] == "full" + consume(first) + # End the earlier work Turn through its real owner. Fresh admission below + # does not bypass an unfinished decision's original-context recovery. + rc, refreshed = _run_cli(registry, runtime, "refresh-state", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "native-work-first", + "--classification", "fixture_work_validated", "--delivery-batch-scale", "single_surface", + "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--progress-result-class", "advanced", "--progress-work-item-id", TODO_ID, + "--progress-evidence-id", "evidence:fixture-work", cwd=project) + assert rc == 0 and refreshed["ok"], refreshed + rc, spent = _run_cli(registry, runtime, "quota", "spend-slot", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "native-work-first", + "--source", "heartbeat", "--execute", cwd=project) + assert rc == 0 and spent["ok"], spent + original = Path(first["recovery"]["path"]).read_bytes() + context.codex_context_hook(str(root), {**event, "source": "resume"}) + assert "candidate" not in context._state(root) + + current = guard("native-work-next") + assert current["mode"] == "full" and "candidate" not in context._state(root) + body = consume(current) + assert body["heartbeat_receipt"]["turn_instance_id"] == "native-work-next" + assert context._state(root)["candidate"]["target"] == current["target"] + assert Path(first["recovery"]["path"]).read_bytes() == original + warm = guard("native-work-next") + assert warm["mode"] == "delta" and warm["target"] != current["target"] + assert _heartbeat_receipt_count(runtime, "native-work-first") == 1 + assert _heartbeat_receipt_count(runtime, "native-work-next") == 1 + assert _spend_run_count(runtime) == 1 + + @pytest.mark.parametrize("native_failure", [None, "projection", "directory"]) def test_native_projection_never_layers_host_projection(tmp_path, monkeypatch, native_failure): from loopx.cli import build_parser From ef805b4ec6cddcf7d37003a4e4aea3b6515d8084 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:25:38 +0800 Subject: [PATCH 18/40] test(workspace): check model readback in the shipped composer Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../dashboard/smoke/chat-model-menu-smoke.mjs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs b/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs index 4835f564bc..f548ade959 100644 --- a/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs +++ b/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs @@ -3,12 +3,12 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { ChatModelMenu } from "../src/features/personal-workspace/chat-model-menu.tsx"; import { ChannelHeader } from "../src/features/personal-workspace/channel-header.tsx"; +import { ComposerRuntimeControls } from "../src/features/personal-workspace/composer-controls.tsx"; import { WorkspaceI18nProvider } from "../src/features/personal-workspace/i18n.tsx"; // Presentation semantics only. Real width, focus, IME and native delivery still // belong to the packaged browser and production HTTP/provider checks. const originalWindow = globalThis.window; -let assertions = 0; try { for (const locale of ["zh-CN", "en"]) { globalThis.window = { localStorage: { getItem: () => locale } }; @@ -61,7 +61,6 @@ try { assert.doesNotMatch(header("error"), /]*disabled=""/, "A failed refresh can be retried"); const stewardProps = { agents: [{ agentId: "codex", label: "Codex", available: true }], selectedAgentId: "codex", - selectedAgentModelReadback: { agent_model_id: "goal-only-model", agent_reasoning_effort: "high", agent_service_tier: "priority" }, managerChannelBinding: { executor_endpoint: "codex", executor_kind: "individual", model: "gpt-6.1-sol", reasoning_effort: "xhigh", selection_policy: "preferred", allocation_reason: "product_default" }, managerRuntime: { runtime_profile: "trusted_owner", sandbox: "danger-full-access", status: "ready" }, onOpenManagerSettings() {}, @@ -73,9 +72,24 @@ try { assert.doesNotMatch(steward, /goal-only-model|Fast|personal-agent-select/, "The steward has one model owner, without Goal runtime/default controls"); const restricted = header("idle", { ...stewardProps, managerRuntime: { runtime_profile: "restricted", sandbox: "read-only", status: "ready" } }); assert.match(restricted, /只读|Read-only/); - const goal = header("idle", { ...stewardProps, selectedGoal: { goalId: "public-case", title: "Public Goal", agentLanes: [] } }); - assert.doesNotMatch(goal, /goal-only-model|Fast|personal-chat-model-menu/, "The Goal reading header leaves sending choices in the single composer"); - assert.doesNotMatch(goal, /gpt-6\.1-sol/, "Goal readback does not inherit the steward model"); + const goalHeader = header("idle", { ...stewardProps, selectedGoal: { goalId: "public-case", title: "Public Goal", agentLanes: [] } }); + assert.doesNotMatch(goalHeader, /goal-only-model|Fast|personal-chat-model-menu|personal-agent-select/, "Goal sending controls belong to the composer, and its header never inherits the steward binding"); + assert.doesNotMatch(goalHeader, /gpt-6\.1-sol/, "Goal readback does not inherit the steward model"); + const composer = (overrides = {}) => renderToStaticMarkup(createElement(WorkspaceI18nProvider, null, + createElement(ComposerRuntimeControls, { agents: stewardProps.agents, agentId: "codex", goalId: "public-case", + original: false, model: "next-model", effort: "low", onAgent() {}, onModel() {}, onEffort() {}, onDefaults() {}, + readback: { agent_model_id: "goal-only-model", agent_reasoning_effort: "high", agent_service_tier: "priority" }, ...overrides }))); + const goal = composer(); + assert.match(goal, /goal-only-model/); + assert.match(goal, /Fast/); + assert.match(goal, new RegExp(defaultsLabel), "Goal defaults stay discoverable beside the actual model readback"); + const composerSummary = goal.slice(goal.indexOf("")); + assert.doesNotMatch(composerSummary, /next-model|low/, "The shipped composer forwards actual Session readback separately from pending configuration"); + const statusComposer = composer({ agentId: "status-only" }); + assert.doesNotMatch(statusComposer, /goal-only-model|Fast|personal-chat-model-menu/, "Status-only has no model Session to report or configure"); + const originalComposer = composer({ original: true }); + assert.match(originalComposer, /goal-only-model/, "Original conversation controls still show their observed Session"); + assert.doesNotMatch(originalComposer, new RegExp(`${defaultsLabel}|${locale === "zh-CN" ? "新对话配置" : "New conversation settings"}|personal-agent-select`), "Original conversation controls do not offer another executor or ordinary conversation defaults"); const linked = header("idle", { readOnlySourceLabel: "Historical run", selectedGoal: { goalId: "source-case", title: "Historical run", agentLanes: [] } }); // The persistent badge is its trigger name and visible text. Its disclosure // panel is opened on demand and may explain the same source once. @@ -87,7 +101,7 @@ try { assert.doesNotMatch(badge, /Historical run/, "The read-only source badge does not repeat the visible Goal title"); const distinct = header("idle", { readOnlySourceLabel: "Remote build", selectedGoal: { goalId: "source-case", title: "Public Goal", agentLanes: [] } }); assert.match(distinct, /class="personal-read-only-source-label"[^>]*>Remote build Date: Mon, 26 Oct 2026 13:02:31 +0800 Subject: [PATCH 19/40] test(context): isolate recovery commands from following guidance Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_codex_context_capture.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index fc5a69ecd2..f738d39048 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -297,7 +297,9 @@ def response(request, number, message): # the fixture may not supply a hidden saved capture path. prefix = "If continuing a captured LoopX decision" permanent = text[text.index(prefix):] - cmd = permanent.split("with: ", 1)[1].split(". Follow ", 1)[0] + # Read only the advertised command sentence. Subsequent + # recovery guidance is not part of the shell invocation. + cmd = permanent.split("with: ", 1)[1].split(". ", 1)[0] stages["phase"] = "lookup_read" assert delivered(request, "cold") == "" and delivered(request, "warm") == "" return tool(cmd, number) From 0799a46e4fe058ad285576a6b6eca8bf350da378 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:54:54 +0800 Subject: [PATCH 20/40] perf(context): reuse exact baseline strings after relocation Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 10 +++ loopx/control_plane/quota/context_delivery.ts | 62 ++++++++++++++----- .../control_plane_ts/context_delivery.test.ts | 42 +++++++++++++ 3 files changed, 97 insertions(+), 17 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 07b6df27c5..58ae5a4ff8 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -162,6 +162,16 @@ input; grouping saves repeated wrappers rather than omitting controls. The codec compares this encoding with ordinary edits and full replacement before choosing. A reader that does not recognize the edit recovers the full capture. +Eligible reusable strings may also use `from`, an array of object keys or exact +array indices in the immutable base capture. This avoids resending identical +authoring when task binding or source ordering moves it to another field. The +reference copies the complete exact string, never a similar string or a value +from the current patch. Current and unknown controls remain inline. Missing, +malformed or incompatible references recover the same full current capture; +the reconstructed context must still match its complete digest. Cold delivery, +session invalidation and authority remain unchanged. This saves transport only; +it does not establish model retention, billed-token savings or useful outcomes. + These are presentation rules within the existing default-off option, not a new configuration or admission policy. A missing continuity basis, a changed epoch, a damaged patch or an incompatible reader automatically recovers the same original full diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 3c04a34607..46fabffe97 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -148,7 +148,7 @@ function splitContext(context: JsonObject): Split { * Keep one exact codec for quota and Turn; source-only callers use the same codec. */ const SURFACES = ["work_context", "quota_packet", "turn_envelope"] as const; type Surface = typeof SURFACES[number]; -type Delta = {value: unknown} | {changed?: Record; values?: Record; removed?: string[]} +type Delta = {value: unknown} | {from: Path} | {changed?: Record; values?: Record; removed?: string[]} | {items: Record; values?: Record; length?: number}; const pathKey = (path: Path): string => JSON.stringify(path); @@ -279,12 +279,12 @@ function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: re /** Group complete CURRENT literals, not baseline references. Keep the cheaper * ordinary edit when grouping would grow metadata; both containers share this rule. */ -function groupCurrentValues(delta: Exclude): Delta { +function groupCurrentValues(delta: Exclude): Delta { const children = Object.entries("items" in delta ? delta.items : delta.changed ?? {}); const values = children.filter(([, child]) => "value" in child); if (values.length < 2) return delta; const nested = Object.fromEntries(children.filter(([, child]) => !("value" in child))); - const grouped: Exclude = { ...delta, + const grouped: Exclude = { ...delta, ...("items" in delta ? {items: nested} : {changed: nested}), values: Object.fromEntries(values.map(([key, child]) => [key, (child as {value: unknown}).value])), }; @@ -295,27 +295,42 @@ function groupCurrentValues(delta: Exclude): Delta { function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | null { const exact = new Set(forced.map(pathKey)), ancestors = new Set(); for (const path of forced) for (let i = 0; i < path.length; i++) ancestors.add(pathKey(path.slice(0, i))); + // Binding or source reordering can relocate already-read authoring. Index + // exact strings once; a reusable target may copy them from the immutable + // base. Forced current controls still take precedence over this index. + const strings = new Map(); + const index = (value: unknown, path: Path) => { + if (typeof value === "string") { + const reference = {from: path}, previous = strings.get(value); + if (bytes(reference) < bytes({value}) && (!previous || bytes(reference) < bytes(previous))) + strings.set(value, reference); + } else if (Array.isArray(value)) value.forEach((v, i) => index(v, [...path, i])); + else if (jsonObject(value)) Object.entries(value as JsonObject).forEach(([k, v]) => index(v, [...path, k])); + }; + index(before, []); const visit = (a: unknown, b: unknown, path: Path): Delta | null => { const key = pathKey(path); if (exact.has(key)) return {value: clone(b)}; if (!ancestors.has(key) && equal(a, b)) return null; + if (typeof b === "string" && strings.has(b)) return clone(strings.get(b)!); let delta: Delta; - if (jsonObject(a) && jsonObject(b)) { - const old = a as JsonObject, next = b as JsonObject; + if ((jsonObject(a) || a === undefined) && jsonObject(b)) { + const old = (a ?? {}) as JsonObject, next = b as JsonObject; const changed: [string, Delta][] = []; for (const [field, value] of Object.entries(next)) { - const child = Object.hasOwn(old, field) ? visit(old[field], value, [...path, field]) : {value: clone(value)}; + const child = visit(Object.hasOwn(old, field) ? old[field] : undefined, value, [...path, field]); if (child) changed.push([field, child]); } const removed = Object.keys(old).filter(field => !Object.hasOwn(next, field)); delta = groupCurrentValues({changed: Object.fromEntries(changed), ...(removed.length ? {removed} : {})}); - } else if (Array.isArray(a) && Array.isArray(b)) { + } else if ((Array.isArray(a) || a === undefined) && Array.isArray(b)) { + const old = a as unknown[] | undefined; const items: [string, Delta][] = []; b.forEach((value, i) => { - const child = i < a.length ? visit(a[i], value, [...path, i]) : {value: clone(value)}; + const child = visit(old?.[i], value, [...path, i]); if (child) items.push([String(i), child]); }); - delta = groupCurrentValues({items: Object.fromEntries(items), ...(a.length !== b.length ? {length: b.length} : {})}); + delta = groupCurrentValues({items: Object.fromEntries(items), ...(old?.length !== b.length ? {length: b.length} : {})}); } else return {value: clone(b)}; // Sizing does not publish a value. Copy only a replacement that is chosen, // rather than every unchanged subtree and every discarded ancestor value. @@ -326,25 +341,38 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu /** Array indices address the exact hashed baseline, not inferred entity ids. * Reorders, duplicate values and length changes are lossless structural edits. - * Full strings are replaced; no timestamp/revision normalization or prose diff. */ -function applyTree(before: unknown, delta: Delta | null): unknown { + * A relocated string may reference the exact immutable base. No timestamp, + * revision normalization, fuzzy identity or prose diff is involved. */ +function applyTree(before: unknown, delta: Delta | null, baseline: unknown = before): unknown { if (delta === null) return clone(before); if ("value" in delta) return clone(delta.value); + if ("from" in delta) { + if (Object.keys(delta).length !== 1 || !Array.isArray(delta.from)) throw new Error("invalid string reference"); + let value = baseline; + for (const key of delta.from) { + if (!(typeof key === "string" || typeof key === "number" && Number.isSafeInteger(key) && key >= 0) + || value === null || typeof value !== "object" || !Object.hasOwn(value, key)) + throw new Error("string reference is not an own baseline path"); + value = (value as Record)[key]; + } + if (typeof value !== "string") throw new Error("string reference has no exact baseline string"); + return value; + } if ("items" in delta) { - if (!Array.isArray(before)) throw new Error("array baseline missing"); - const result = clone(before); + if (before !== undefined && !Array.isArray(before)) throw new Error("array baseline missing"); + const source = (before ?? []) as unknown[], result = clone(source); if (delta.length !== undefined) result.length = delta.length; for (const [key, value] of Object.entries(delta.values ?? {})) result[Number(key)] = clone(value); - for (const [key, item] of Object.entries(delta.items)) result[Number(key)] = applyTree(before[Number(key)], item); + for (const [key, item] of Object.entries(delta.items)) result[Number(key)] = applyTree(source[Number(key)], item, baseline); return result; } - const source = requireJsonObject(before, "object baseline"), result = clone(source); + const source = before === undefined ? {} : requireJsonObject(before, "object baseline"), result = clone(source); for (const key of delta.removed ?? []) delete result[key]; for (const [key, value] of Object.entries(delta.values ?? {})) Object.defineProperty(result, key, {value: clone(value), enumerable: true, writable: true, configurable: true}); for (const [key, item] of Object.entries(delta.changed ?? {})) { // Avoid prototype setters for JSON property names such as __proto__. - Object.defineProperty(result, key, {value: applyTree(Object.hasOwn(source, key) ? source[key] : undefined, item), + Object.defineProperty(result, key, {value: applyTree(Object.hasOwn(source, key) ? source[key] : undefined, item, baseline), enumerable: true, writable: true, configurable: true}); } return result; @@ -398,7 +426,7 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r ...(assumed ? {continuity_basis: "native_hook_assumption"} : {}), instruction: "Current controls are inline even when unchanged. Other fields inherit the " + (assumed ? "exact baseline under the native-hook continuity assumption" : "confirmed baseline") - + "; all requirements remain binding. A value replaces its subtree; values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; + + "; all requirements remain binding. A value replaces its subtree; from copies the exact string at that path in the base capture, never in current changes. Values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; if (bytes(delta) >= bytes(full("delta_not_smaller"))) return full("delta_not_smaller"); if (!equal(applyTree(prior, changes), context)) return full("reconstruction_failed"); // Native consumers page the wire before they can use its baseline. Deliver diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index 490bc14326..dba02c56dd 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -102,6 +102,48 @@ test("fresh control with exact warm bodies; changed source revisions do not rese assert.deepEqual(baseline.context, original); }); +test("relocated reusable strings reference the exact base while current controls stay inline", () => { + const original = {registry: {authoring: long}, current_control: {instruction: long}}; + const current = {registry: {attachment: {guidance: long}}, current_control: {instruction: long}}; + const initial = prepareContextDelivery({context: original, scope, capture_id: "before", surface: "quota_packet"}); + const args = {context: current, scope, capture_id: "after", surface: "quota_packet"}; + const delta = prepareContextDelivery({...args, baseline: initial, retained: {complete: true, target: initial.target}}); + assert.equal(delta.mode, "delta"); + const tree = delta.changes as {changed: {registry: {changed: {attachment: {changed: {guidance: unknown}}}}}}; + assert.deepEqual(tree.changed.registry.changed.attachment.changed.guidance, {from: ["registry", "authoring"]}); + assert.deepEqual(inline(delta, ["current_control"]), current.current_control); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline: initial, + current: prepareContextDelivery(args)}).context, current); + assert.deepEqual(initial.context, original); + assert.deepEqual(prepareContextDelivery(args).context, current); // cold delivery remains literal + + // References never read the patch being applied, inherited properties, or a + // different string. Any damaged reference recovers this exact current capture. + for (const reference of [["registry", "missing"], ["constructor", "name"], [], ["current_control"], ["registry", -1], "registry.authoring"]) { + const damaged = structuredClone(delta); + const node = (damaged.changes as typeof tree).changed.registry.changed.attachment.changed; + node.guidance = {from: reference}; + const restored = restoreContextDelivery({delivery: damaged, baseline: initial, current: prepareContextDelivery(args)}); + assert.equal(restored.mode, "full"); + assert.deepEqual(restored.context, current); + } +}); + +test("new reusable arrays can relocate exact strings without copying current edits", () => { + const original = {registry: {text: long, other: long + " old"}}; + const current = {registry: {items: [long + " old", long, long + " new"]}}; + const initial = prepareContextDelivery({context: original, scope, capture_id: "before", surface: "quota_packet"}); + const args = {context: current, scope, capture_id: "after", surface: "quota_packet"}; + const delta = prepareContextDelivery({...args, baseline: initial, retained: {complete: true, target: initial.target}}); + assert.equal(delta.mode, "delta"); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline: initial, + current: prepareContextDelivery(args)}).context, current); + const wire = JSON.stringify(delta.changes); + assert.match(wire, /"from"/); + assert.match(wire, / new/); // changed requirements are literal, never fuzzy matches + assert.ok(wire.length < JSON.stringify(current).length / 2); +}); + test("exact-base source and requirement reorders preserve current positions", () => { const initial = context(); const outcomes = ((source(initial, 2).goal_acceptance_contract as JsonObject).outcome_requirements as JsonObject).outcomes as JsonObject[]; From 0ed447db17e8d2fcf22ff7941ac85a0cacbd9d6c Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:36:13 +0800 Subject: [PATCH 21/40] test(context): keep machine discovery assertions capability scoped Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_periodic_report_machine_store.py | 18 ++++++++---------- .../test_context_machine_configuration.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/capabilities/test_periodic_report_machine_store.py b/tests/capabilities/test_periodic_report_machine_store.py index f66921eb66..9b7a09c02c 100644 --- a/tests/capabilities/test_periodic_report_machine_store.py +++ b/tests/capabilities/test_periodic_report_machine_store.py @@ -401,16 +401,14 @@ def test_canonical_machine_config_cli_uses_the_same_store_and_projection( assert main([*common, "describe"]) == 0 catalog = json.loads(capsys.readouterr().out) assert catalog["schema_version"] == "machine_configuration_catalog_v0" - assert [item["namespace"] for item in catalog["namespaces"]] == [ - "change_quality_qualification", - "codex_chat", - "goal_storage", - "manager_runtime", - "periodic_report", - "pull_request_review", - "steward_executor", - "todo_replan_cadence", - ] + namespaces = [item["namespace"] for item in catalog["namespaces"]] + assert namespaces == sorted(set(namespaces)) + # Other capabilities may register independently; this integration owns the + # periodic-report discovery contract and its subsequent preview/apply path. + reports = [item for item in catalog["namespaces"] if item["namespace"] == "periodic_report"] + assert len(reports) == 1 + assert reports[0]["schema_versions"] == ["periodic_report_machine_defaults_v0"] + assert reports[0]["configuration_template"]["enabled"] is False assert ( main( diff --git a/tests/control_plane/test_context_machine_configuration.py b/tests/control_plane/test_context_machine_configuration.py index b978aa8e3a..4a941accab 100644 --- a/tests/control_plane/test_context_machine_configuration.py +++ b/tests/control_plane/test_context_machine_configuration.py @@ -31,6 +31,20 @@ def configure(registry, runtime, root, mode): "--expected-plan-revision", preview["plan_revision"], "--execute") +def test_machine_catalog_discovers_context_delivery_without_activation(tmp_path): + registry, runtime = tmp_path / "absent.json", tmp_path / "runtime" + code, catalog = _run_cli(registry, runtime, "machine-config", "describe") + assert code == 0 and catalog["schema_version"] == "machine_configuration_catalog_v0", catalog + contexts = [item for item in catalog["namespaces"] if item["namespace"] == "context_delivery"] + assert len(contexts) == 1 + assert contexts[0]["schema_versions"] == [CONTEXT_DELIVERY_MACHINE_SCHEMA] + assert contexts[0]["configuration_template"] == { + "schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, "mode": "off", + } + assert not registry.exists() and not (runtime / "machine/configuration.json").exists() + assert not (runtime / "context-delivery").exists() + + @pytest.mark.parametrize("invalid", [True, False, None, 1, "auto", "codex_app_heartbeat"]) def test_invalid_machine_mode_never_writes(tmp_path, invalid): registry, runtime = tmp_path / "absent.json", tmp_path / "runtime" From bed371413798206ea59ce479e1c947b566137e83 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:03:34 +0800 Subject: [PATCH 22/40] fix(context): preserve complete generic read guidance Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane/turn_driver/decision_capture.py | 15 ++++++++++----- tests/control_plane/test_saved_context_project.py | 13 +++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index d7d8e74816..f90a47d65a 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -116,13 +116,18 @@ def validate_handoff_socket(value: str) -> Path: "Do not rerun admission or ask the owner. File existence and page EOF are not retention proof." ) # Native paging verifies capture bytes; the existing codec separately declares -# its continuity basis. Keep the strict caller's original guidance unchanged. +# its continuity basis. Generic/off readers retain their original obligations +# without claiming that a native input observer exists. CONTEXT_CAPTURE_PAGE_GUIDANCE = _CONTEXT_CAPTURE_PAGE_START + _CONTEXT_CAPTURE_PAGE_END CONTEXT_CAPTURE_READ_GUIDANCE = ( - _CONTEXT_CAPTURE_PAGE_START - + "Native input observation independently verifies complete model-visible " - "bytes before delta reuse; neither a retyped hash nor a disk hash proves that input. " - + _CONTEXT_CAPTURE_PAGE_END + "Follow next_command through eof and combine content in byte-offset order. " + "In code-mode, forward successful exec stdout directly with text(result.output); " + "retain errors. Small batches can save model round trips; start with at most two default pages " + "and inspect every output. A host may truncate inside valid JSON or omit a whole output. " + "Check each model-visible content's UTF-8 byte length against next_offset (total_bytes at eof), " + "complete coverage and the original SHA-256. If incomplete, reread the affected range " + "of this same capture with fewer pages or a smaller --page-bytes; do not trust a preview or EOF alone. " + "Do not rerun admission or ask the owner. File existence and page EOF are not retention proof." ) diff --git a/tests/control_plane/test_saved_context_project.py b/tests/control_plane/test_saved_context_project.py index 398bcbae18..0e6643b227 100644 --- a/tests/control_plane/test_saved_context_project.py +++ b/tests/control_plane/test_saved_context_project.py @@ -81,7 +81,7 @@ def test_enabled_cache_preparation_failure_returns_error_with_original_reference -@pytest.mark.parametrize("enabled", [False, True]) +@pytest.mark.parametrize("enabled", [None, False, True], ids=["omitted", "off", "on"]) @pytest.mark.parametrize("cache_location", ["default", "new", "existing"]) def test_saved_projection_manages_private_cache_without_authority_io(tmp_path, monkeypatch, enabled, cache_location): from loopx.control_plane.turn_driver.codex_context import read_codex_context_reference @@ -106,7 +106,9 @@ def test_saved_projection_manages_private_cache_without_authority_io(tmp_path, m registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" args = ["quota", "context-project", "--capture-file", str(path), "--capture-sha256", digest, "--goal-id", "saved-goal", "--agent-id", "saved-agent", "--turn-instance-id", "saved-turn", - "--codex-app", "--context-delivery", "on" if enabled else "off", "--codex-context-dir", str(context)] + "--codex-app", "--codex-context-dir", str(context)] + if enabled is not None: + args += ["--context-delivery", "on" if enabled else "off"] if cache_location != "default": args += ["--decision-output-root", str(root)] code, result = _run_cli(registry, runtime, *args, cwd=tmp_path) @@ -129,6 +131,13 @@ def test_saved_projection_manages_private_cache_without_authority_io(tmp_path, m assert "content" not in result and "schema_version" not in result assert not list(context.iterdir()) assert root.exists() == (cache_location == "existing") + # A generic reader has no native observer. Keep its complete-read + # obligations instead of promising an input proof that never ran. + instruction = result["instruction"] + assert "Native input observation" not in instruction + assert "Check each model-visible content's UTF-8 byte length" in instruction + assert "complete coverage and the original SHA-256" in instruction + assert "If incomplete, reread the affected range of this same capture" in instruction command = shlex.split(result["command"]) code, page = _run_cli(registry, runtime, *command[3:], cwd=tmp_path) assert code == 0 and json.loads(page["content"]) == original From 939019f7b64726e5dcb57ad1d15823d9b2428ef9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:30:03 +0800 Subject: [PATCH 23/40] perf(context): reuse subtree byte sizes during diff selection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/quota/context_delivery.ts | 30 +++++++++++++++---- .../control_plane_ts/context_delivery.test.ts | 5 +++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 46fabffe97..e31d1277a1 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -279,7 +279,7 @@ function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: re /** Group complete CURRENT literals, not baseline references. Keep the cheaper * ordinary edit when grouping would grow metadata; both containers share this rule. */ -function groupCurrentValues(delta: Exclude): Delta { +function groupCurrentValues(delta: Exclude, size: (value: unknown) => number): Delta { const children = Object.entries("items" in delta ? delta.items : delta.changed ?? {}); const values = children.filter(([, child]) => "value" in child); if (values.length < 2) return delta; @@ -289,10 +289,30 @@ function groupCurrentValues(delta: Exclude [key, (child as {value: unknown}).value])), }; if (!("items" in grouped) && !Object.keys(nested).length) delete grouped.changed; - return bytes(grouped) < bytes(delta) ? grouped : delta; + return size(grouped) < size(delta) ? grouped : delta; } function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | null { + // Contexts have passed strict JSON digest validation; patch containers remain + // immutable after sizing. Reuse child sizes within this one tree calculation. + const sizes = new WeakMap(); + const size = (value: unknown): number => { + if (value === null || typeof value !== "object") return bytes(value); + const prior = sizes.get(value); + if (prior !== undefined) return prior; + let total = 2, count = 0; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) total += (i ? 1 : 0) + + (value[i] === undefined ? 4 : size(value[i])); + } else { + for (const [key, item] of Object.entries(value)) { + if (item === undefined) continue; + total += (count++ ? 1 : 0) + bytes(key) + 1 + size(item); + } + } + sizes.set(value, total); + return total; + }; const exact = new Set(forced.map(pathKey)), ancestors = new Set(); for (const path of forced) for (let i = 0; i < path.length; i++) ancestors.add(pathKey(path.slice(0, i))); // Binding or source reordering can relocate already-read authoring. Index @@ -322,7 +342,7 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu if (child) changed.push([field, child]); } const removed = Object.keys(old).filter(field => !Object.hasOwn(next, field)); - delta = groupCurrentValues({changed: Object.fromEntries(changed), ...(removed.length ? {removed} : {})}); + delta = groupCurrentValues({changed: Object.fromEntries(changed), ...(removed.length ? {removed} : {})}, size); } else if ((Array.isArray(a) || a === undefined) && Array.isArray(b)) { const old = a as unknown[] | undefined; const items: [string, Delta][] = []; @@ -330,11 +350,11 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu const child = visit(old?.[i], value, [...path, i]); if (child) items.push([String(i), child]); }); - delta = groupCurrentValues({items: Object.fromEntries(items), ...(old?.length !== b.length ? {length: b.length} : {})}); + delta = groupCurrentValues({items: Object.fromEntries(items), ...(old?.length !== b.length ? {length: b.length} : {})}, size); } else return {value: clone(b)}; // Sizing does not publish a value. Copy only a replacement that is chosen, // rather than every unchanged subtree and every discarded ancestor value. - return bytes(delta) < bytes({value: b}) ? delta : {value: clone(b)}; + return size(delta) < size({value: b}) ? delta : {value: clone(b)}; }; return visit(before, after, []); } diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index dba02c56dd..69352cb449 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -459,7 +459,10 @@ test("array edits preserve append, removal, reorder, duplicate entries and exact const before = packet(), baseline = packetFull(before); const old = (before.planning_horizon as JsonObject).work_items as JsonObject[]; const cases: unknown[] = [[], [old[1]], [...old, {id: "three", body: long}], [old[1], old[0]], - [old[0], old[0]], [null, "", false, 0], null, "", {items: old}, + [old[0], old[0]], [null, "", false, 0], + ["\"\\\n\t\u0000", "完整🙂", "\ud800", "\udfff", 1e-7, 1e21], + [{"\"\\\n\u0000": [null, false, "完整🙂"], "\ud800": {values: [], items: {}}}], + null, "", {items: old}, [{...old[0], late_constraint: "STOP now", revision: "r2"}, old[1]]]; for (const items of cases) { const current = {...before, planning_horizon: {work_items: items}}; From 3b32f2e5530728f7db94bac7383e8e5f22bd9dac Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:40:08 +0800 Subject: [PATCH 24/40] perf(context): bound native reader pages by serialized output bytes Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 36 ++++-- loopx/cli_commands/quota_capture.py | 72 ++++++++++-- loopx/cli_commands/quota_registration.py | 3 + .../turn_driver/codex_context.py | 11 +- .../turn_driver/decision_capture.py | 18 ++- .../test_codex_context_capture.py | 66 +++++++---- .../test_context_delivery_cli.py | 107 ++++++++++++++++++ 7 files changed, 264 insertions(+), 49 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 58ae5a4ff8..8390f92029 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -221,9 +221,12 @@ setting. Ordinary machine validation still applies to business operations. 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. Native pages stay at 3 KiB throughout that -read, reducing output-wrapper truncation; consume the inline page before following -next_command. Forward exec stdout directly in code mode. Repeating the packet in +recovery without another admission. The inline page stays at 3 KiB. Continuations +request up to 8 KiB of content with `--max-page-output-bytes 7000`, 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, +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 a hashing tool call does not establish that its original bytes were retained. The reader verifies the immutable capture digest. Native lifecycle delivery uses the same byte-range validator as explicit input observations: before merging @@ -383,8 +386,10 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 机器开启后,`quota context-config --codex-app --goal-id GOAL --agent-id AGENT` 只准备 私有传输末级目录并生成已有 hook 和恢复指令,不安装或信任 hook,也不更改宿主配置。 原生 `quota should-run` 与已保存捕获的 `context-project` 共用分页呈现,直接返回第零页、 -固定摘要的后续读取与完整恢复,不另行准入。该原生读取链的页大小保持 3 KiB,以减少 -输出包装造成的截断;先消费已附带页,再跟随 next_command。Code Mode 直接转发 +固定摘要的后续读取与完整恢复,不另行准入。内联页保持 3 KiB;后续命令最多请求 +8 KiB 正文,并用 `--max-page-output-bytes 7000` 将成功 JSON stdout(含转义、 +后续命令与换行)限制为 7000 字节。先消费内联页,再跟随 next_command;保留历史 +上限未知时,每次只读一个有界输出。该上限不证明模型保留。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 读取器校验不可变 capture 的摘要。Post-tool 适配层复用显式输入观察的字节范围校验, 先将实际 UTF-8 正文与可信 journal 指向的物理捕获及固定摘要核对,再合并读取范围。 @@ -441,13 +446,20 @@ 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 -to leave room for projection metadata; subsequent native pages also use 3 KiB. +to leave room for projection metadata. Subsequent native commands request up to +8 KiB of content within a 7000-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 +without a zero-progress page; increase it for the same capture. Omitting the flag +preserves legacy reader output. Error messages are not subject to an impossible +page limit, and this option changes no host or retained-history budget. 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 output. If the optional first-page read fails, that same command remains usable. -Off responses do not add a page. No new flag, admission or retention receipt is -introduced; an inline page alone cannot prove complete input retention. +Off responses do not add a page or change their existing guidance. No admission +or retention receipt is introduced; an inline page alone cannot prove complete +input retention. `quota context-project` 开启上下文投递后,不再要求另外手工准备缓存目录。 省略 `--decision-output-root` 时复用原生会话的私有上下文目录;显式指定的新目录 @@ -457,9 +469,13 @@ introduced; an inline page alone cannot prove complete input retention. 原始摘要和 Turn 回执,不重新准入或扣减额度。 开启时,成功的已保存投影同时用现有 `context_capture_page_v1` 字段返回第一页。 -该页内容最多 3 KiB,为投影元数据留出输出空间;后续原生页也按 3 KiB 读取。 +该页内容最多 3 KiB,为投影元数据留出输出空间;后续原生命令最多请求 8 KiB +正文,成功 JSON stdout 限制为 7000 字节。可选的 JSON 专用参数 +`--max-page-output-bytes` 按 UTF-8 边界缩减正文,并在 `next_command` 中保留。 +上限不足时返回错误,不返回无进展的分页;增大上限后重读同一捕获。省略该参数 +保持旧读取输出。错误消息不受无法满足的分页上限限制,也不更改宿主或历史输出预算。 先读取 `content`,再沿 `next_command` 读至 `eof`,确保完整字节覆盖。 读取器校验原始摘要;无需重抄正文或计算哈希。 原有 `command` 仍从零开始,供旧读取方或内联输出不完整时重读;可选的首屏读取 -失败时也保留该命令。关闭态不增加分页数据,不新增开关、准入或保留回执; +失败时也保留该命令。关闭态不增加分页数据,也不改变既有指引;不新增准入或保留回执; 单页内联内容不证明完整输入仍被保留。 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index 3cae48c410..ce91fc9bc3 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -18,6 +18,7 @@ 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, read_context_capture_file as _read_capture, ) @@ -31,6 +32,49 @@ def native_context_activation(args: argparse.Namespace) -> dict[str, Any]: raise QuotaCommandValidationError(str(exc)) from exc +def _capture_page(path: Path, raw: bytes, digest: str, *, offset: int, size: int, + output_limit: int | None, instruction: bool) -> dict[str, Any]: + end = min(offset + size, len(raw)) + while end < len(raw) and raw[end] & 0xC0 == 0x80: + end -= 1 + content = raw[offset:end].decode("utf-8") + + def page(text: str) -> dict[str, Any]: + cursor = offset + len(text.encode("utf-8")) + result: dict[str, Any] = {"ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, + "offset": offset, "next_offset": cursor if cursor < len(raw) else None, + "total_bytes": len(raw), "eof": cursor == len(raw), "content": text} + if instruction: + result["read_instruction"] = CONTEXT_CAPTURE_PAGE_GUIDANCE + if cursor < len(raw): + result["next_command"] = context_capture_read_command(path, digest, offset=cursor, page_bytes=size, + max_page_output_bytes=output_limit) + return result + + def fits(result: dict[str, Any]) -> bool: + # Match cli_runtime.print_payload exactly, including print's newline. + return output_limit is None or len(json.dumps(result, ensure_ascii=False, + separators=(",", ":")).encode("utf-8")) + 1 <= output_limit + + result = page(content) + if fits(result): + return result + # EOF drops next_command: test it above before searching the monotone + # non-EOF prefix sizes. Never return a zero-progress continuation. + low, high = 1, len(content) - (end == len(raw)) + best = None + while low <= high: + middle = (low + high) // 2 + candidate = page(content[:middle]) + if fits(candidate): + best, low = candidate, middle + 1 + else: + high = middle - 1 + if best is None: + raise ValueError("max-page-output-bytes cannot fit page metadata and one UTF-8 character; increase the limit for this same capture") + return best + + def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: """Bounded local IO over immutable JSON, not a new context/authority owner. @@ -42,6 +86,10 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: digest = getattr(args, "capture_sha256", None) value = getattr(args, "capture_file", None) offset, size = args.offset, args.page_bytes + output_limit = getattr(args, "max_page_output_bytes", None) + if output_limit is not None and (type(output_limit) is not int or output_limit <= 0 + or getattr(args, "format", None) != "json"): + raise ValueError("max-page-output-bytes requires JSON output and a positive integer") from ..control_plane.turn_driver.codex_context import codex_context_directory context_dir = codex_context_directory(directory=getattr(args, "codex_context_dir", None), @@ -60,17 +108,8 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: path, raw, _ = _read_capture(value, digest) if offset > len(raw) or (offset < len(raw) and raw[offset] & 0xC0 == 0x80): raise ValueError("capture offset is outside the file or not a UTF-8 boundary; use the returned next offset") - end = min(offset + size, len(raw)) - while end < len(raw) and raw[end] & 0xC0 == 0x80: - end -= 1 - result = {"ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, - "offset": offset, "next_offset": end if end < len(raw) else None, - "total_bytes": len(raw), "eof": end == len(raw), "content": raw[offset:end].decode("utf-8")} - if context_dir: - result["read_instruction"] = CONTEXT_CAPTURE_PAGE_GUIDANCE - if end < len(raw): - result["next_command"] = context_capture_read_command(path, digest, offset=end, page_bytes=size) - return result + return _capture_page(path, raw, digest, offset=offset, size=size, + output_limit=output_limit, instruction=bool(context_dir)) 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. @@ -96,11 +135,16 @@ 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")} - reference["command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], page_bytes=3072) + 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) result = {"mode": mode, **reference, **({"target": target} if target else {})} page = read_context_capture(argparse.Namespace(capture_file=reference["path"], capture_sha256=reference["sha256"], offset=0, page_bytes=3072)) if page["ok"]: + if not page["eof"]: + page["next_command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], + offset=page["next_offset"], page_bytes=NATIVE_CONTEXT_PAGE_BYTES, + max_page_output_bytes=NATIVE_CONTEXT_PAGE_OUTPUT_BYTES) result.update(page) from ..control_plane.turn_driver.codex_context import register_native_context_view @@ -381,6 +425,10 @@ 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"}, + args.format, lambda value: str(value["reason"])) + return 1 if registry_path is not None: from .context_machine import apply_context_machine_options diff --git a/loopx/cli_commands/quota_registration.py b/loopx/cli_commands/quota_registration.py index 5d7c008756..b871587048 100644 --- a/loopx/cli_commands/quota_registration.py +++ b/loopx/cli_commands/quota_registration.py @@ -228,6 +228,9 @@ def register_quota_command( quota_parser.add_argument("--offset", type=int, default=0, help="For context-read: byte offset returned by the previous page.") quota_parser.add_argument("--page-bytes", type=int, default=4096, help="For context-read: maximum UTF-8 content bytes per page, 256–8192 (default: 4096).") + 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. " + "Content shrinks at UTF-8 boundaries; next_command preserves the limit. An insufficient limit returns an error.") quota_parser.add_argument( "--turn-instance-id", help=( diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 58ce5125e3..6bbfd3e2ee 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -18,6 +18,7 @@ from .decision_capture import ( 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 adapter activation reuses scheduler-owned host declarations. These @@ -219,13 +220,15 @@ def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_i 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]) + "--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)]) return ( _context_continuation_instructions(command) + "Follow the returned next_command through eof; verify complete bytes and digest. " "For code-mode reads, size batches against both direct-result and retained-history output limits; " - "max_output_tokens alone does not establish the latter. Start with at most two default pages when " - "the effective history limit is unknown. With a known sufficient limit, larger batches can reduce " + "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, " + "start with at most two. With a known sufficient limit, larger batches can reduce " "round trips; emit each stdout separately and check complete model-visible bytes. This advice " "grants no authority to change host limits or treat complete runtime output as retention proof." ) @@ -453,6 +456,8 @@ 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) + 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) 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 f90a47d65a..5ffa40acfd 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -108,7 +108,7 @@ def validate_handoff_socket(value: str) -> Path: "Do not hash or retype the packet. " ) _CONTEXT_CAPTURE_PAGE_END = ( - "Small batches can save model round trips; start with at most two default pages " + "Legacy default pages can be batched to save round trips; start with at most two " "and inspect every output. A host may truncate inside valid JSON or omit a whole output. " "Consume all model-visible content in UTF-8 byte-offset order with complete coverage through total_bytes. " "If a range or output is missing, truncated or uncertain, reread the affected range " @@ -118,7 +118,9 @@ def validate_handoff_socket(value: str) -> Path: # Native paging verifies capture bytes; the existing codec separately declares # its continuity basis. Generic/off readers retain their original obligations # without claiming that a native input observer exists. -CONTEXT_CAPTURE_PAGE_GUIDANCE = _CONTEXT_CAPTURE_PAGE_START + _CONTEXT_CAPTURE_PAGE_END +CONTEXT_CAPTURE_PAGE_GUIDANCE = _CONTEXT_CAPTURE_PAGE_START + ( + "For --max-page-output-bytes, read one output per call if retained-history limits are unknown. " +) + _CONTEXT_CAPTURE_PAGE_END CONTEXT_CAPTURE_READ_GUIDANCE = ( "Follow next_command through eof and combine content in byte-offset order. " "In code-mode, forward successful exec stdout directly with text(result.output); " @@ -131,12 +133,20 @@ def validate_handoff_socket(value: str) -> Path: ) -def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, page_bytes: int = 4096) -> str: +# Leave room below the native 2000-token exec output boundary for framing. +# This does not establish downstream retained-history capacity or retention. +NATIVE_CONTEXT_PAGE_BYTES = 8192 +NATIVE_CONTEXT_PAGE_OUTPUT_BYTES = 7000 + + +def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, page_bytes: int = 4096, + max_page_output_bytes: int | None = None) -> str: from ...command_invocation import loopx_cli_argv return shlex.join([*loopx_cli_argv(), "--format", "json", "quota", "context-read", "--capture-file", str(path), "--capture-sha256", digest, - "--offset", str(offset), "--page-bytes", str(page_bytes)]) + "--offset", str(offset), "--page-bytes", str(page_bytes), + *(["--max-page-output-bytes", str(max_page_output_bytes)] if max_page_output_bytes is not None else [])]) def read_context_capture_file(value: str, digest: str, *, private: bool = False) -> tuple[Path, bytes, dict[str, Any]]: diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index f738d39048..fb919a106e 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -61,6 +61,24 @@ def _delivered(request, digest): return raw.decode().rstrip("\n") +def _last_page(request): + for item in reversed(request["input"]): + output = item.get("output") + parts = [p.get("text", "") for p in output] if isinstance(output, list) else [output] + for part in parts: + if not isinstance(part, str): + continue + try: + value = json.loads(part.split("\nOutput:\n", 1)[-1]) + if isinstance(value, dict) and value.get("exit_code") == 0: + value = json.loads(value["output"]) + if isinstance(value, dict) and value.get("schema_version") == "context_capture_page_v1": + return value + except (ValueError, KeyError): + continue + return None + + @pytest.mark.parametrize("host,surface,failed_primary,resumed,adaptive", [ (host, surface, failed, False, False) for host, surface in [("exec", "quota_packet"), ("app_server", "turn_envelope"), ("code_mode", "quota_packet"), ("code_mode", "turn_envelope")] @@ -69,7 +87,7 @@ def _delivered(request, digest): + [(host, surface, False, True, False) for host in ("benchmark", "code_mode") for surface in ("quota_packet", "turn_envelope")] + [("code_mode", surface, False, True, True) for surface in ("quota_packet", "turn_envelope")]) -def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None): +def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, bounded_pages=False): isolate_sqlite_runtime(tmp_path, monkeypatch) binary, env, workspace, home = _native_fixture(tmp_path) env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) @@ -78,6 +96,8 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" body = "\n".join(f"Requirement {i:04d}: preserve this row and its evidence." for i in range(400)) body += "\nFINAL REQUIREMENT: stop before publication." + if bounded_pages: + body += '\nMixed UTF-8 and JSON escaping: 汉🙂"\\\t' * 100 todos = read_fixture_todos(state) todos[0].update(text=body, title=body) initialize_canonical_authority(runtime, GOAL_ID, @@ -166,6 +186,7 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho env["LOOPX_TURN"] = "native-capture-turn" checkpoint_wakes, held_captures = [], [] + read_commands, bounded_sizes = {}, [] def invoke(name): argv = list(command) @@ -177,6 +198,8 @@ def pages(name, path): raw = path.read_bytes() digest = hashlib.sha256(raw).hexdigest() page_digests[name] = digest + if bounded_pages: + return [read_commands[name] if name != "recovery" else wires["warm"]["recovery"]["command"]] # 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 @@ -219,6 +242,14 @@ def delivered(request, name): def response(request, number, message): nonlocal command, root phase = stages["phase"] + if bounded_pages and phase in {"cold_read", "warm_read", "resume_read", "recovered"}: + 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 <= 7000 + if not page["eof"]: + assert "--max-page-output-bytes 7000" in page["next_command"] + return tool(page["next_command"], number) if stages["pages"]: cmd = stages["pages"].pop(0) elif phase in {"start", "resuming"}: @@ -255,6 +286,7 @@ def response(request, number, message): elif phase in {"cold_ready", "warm_ready"}: name = phase.split("_")[0] descriptor = json.loads(wire_paths[name].read_text()) + 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. wire_file = Path(descriptor["path"]).parent / "delivery.json" @@ -312,24 +344,7 @@ def response(request, number, message): elif phase == "lookup_read": # Product lookup delivered the first page; use only its public # digest-pinned next command, not the private session journal. - page = None - for item in reversed(request["input"]): - output = item.get("output") - parts = [p.get("text", "") for p in output] if isinstance(output, list) else [output] - for part in parts: - if not isinstance(part, str): - continue - try: - value = json.loads(part.split("\nOutput:\n", 1)[-1]) - if isinstance(value, dict) and value.get("exit_code") == 0: - value = json.loads(value["output"]) - if isinstance(value, dict) and value.get("schema_version") == "context_capture_page_v1": - page = value - break - except (ValueError, KeyError): - continue - if page: - break + page = _last_page(request) assert page and page["ok"], page assert page["sha256"] == wires["warm"]["recovery"]["sha256"] page_digests["recovery"] = page["sha256"] @@ -356,7 +371,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, "max_output_tokens": 2400}) + "cmd": command, "max_output_tokens": 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} @@ -557,6 +572,8 @@ def held(*args, **kwargs): assert retained["text"] == body and retained["done"] is False else: assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + if bounded_pages: + assert bounded_sizes and max(bounded_sizes) > 3072 @pytest.mark.parametrize("host,surface,hook_loss", [ @@ -590,6 +607,15 @@ def test_native_shared_configuration_resume_and_lost_hook(tmp_path, monkeypatch, "code_mode", surface, False, True, True, "missing", shared_root=True) +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_bounded_command_chain_recovers_with_unknown_history_limit(tmp_path, monkeypatch, surface): + # 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, + native_read_batch=1, bounded_pages=True) + + @pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) def test_native_benchmark_context_keeps_checkpoint_handoff(tmp_path, monkeypatch, surface): test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 76bb8924ee..c2bfb51a71 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -9,6 +9,9 @@ import json import os from pathlib import Path +import shlex +import subprocess +import sys import pytest @@ -23,6 +26,110 @@ ) +@pytest.mark.parametrize("body", ["plain text " * 900, '汉🙂"\\\t\n' * 700]) +@pytest.mark.parametrize("limit", [1600, 7000]) +def test_reader_bounds_actual_json_stdout_and_preserves_every_byte(tmp_path, monkeypatch, body, limit): + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + capture = tmp_path / "original.json" + raw = (json.dumps({"requirements": body}, ensure_ascii=False) + "\n").encode() + capture.write_bytes(raw) + digest = hashlib.sha256(raw).hexdigest() + command = [sys.executable, "-m", "loopx.entrypoint", "--format", "json", "quota", "context-read", + "--capture-file", str(capture), "--capture-sha256", digest, "--page-bytes", "8192", + "--max-page-output-bytes", str(limit)] + collected, offset, sizes = bytearray(), 0, [] + while True: + response = subprocess.run(command, capture_output=True, cwd=Path(__file__).resolve().parents[2], check=False) + assert response.returncode == 0, response.stderr or response.stdout + assert len(response.stdout) <= limit + page = json.loads(response.stdout) + assert page["ok"] and page["sha256"] == digest and page["offset"] == offset + content = page["content"].encode() + assert content and raw[offset:offset + len(content)] == content + collected.extend(content) + sizes.append(len(content)) + offset += len(content) + assert page["total_bytes"] == len(raw) + assert page["next_offset"] == (None if page["eof"] else offset) + if page["eof"]: + break + command = shlex.split(page["next_command"]) + assert command[command.index("--max-page-output-bytes") + 1] == str(limit) + assert bytes(collected) == raw and capture.read_bytes() == raw + if limit == 7000: + assert max(sizes) > 3072 + # Page reads have no authority or settlement effects. + assert _heartbeat_receipt_count(runtime, "reader-budget") == 0 and _spend_run_count(runtime) == 0 + code, rejected = _run_cli(registry, runtime, "quota", "context-read", "--capture-file", str(capture), + "--capture-sha256", digest, "--max-page-output-bytes", "1") + assert code == 1 and not rejected["ok"] and "same capture" in rejected["reason"] + assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("limit,fmt", [(0, "json"), (-1, "json"), (True, "json"), (1.2, "json"), (7000, "markdown")]) +def test_reader_rejects_invalid_output_budget_before_capture_io(monkeypatch, limit, fmt): + import argparse + from loopx.cli_commands import quota_capture + def unexpected(*args, **kwargs): + pytest.fail("invalid output budget must fail before capture IO") + monkeypatch.setattr(quota_capture, "_read_capture", unexpected) + result = quota_capture.read_context_capture(argparse.Namespace(capture_file="missing", capture_sha256="0" * 64, + offset=0, page_bytes=8192, max_page_output_bytes=limit, format=fmt)) + assert not result["ok"] and "positive integer" in result["reason"] + + +def test_reader_legacy_bytes_and_small_eof_budget(tmp_path): + import argparse + from loopx.cli_commands.quota_capture import read_context_capture + from loopx.control_plane.turn_driver.decision_capture import context_capture_read_command + capture = tmp_path / "original.json" + raw = json.dumps({"late": "🙂" * 400}, ensure_ascii=False).encode() + capture.write_bytes(raw) + digest = hashlib.sha256(raw).hexdigest() + args = argparse.Namespace(capture_file=str(capture), capture_sha256=digest, offset=0, page_bytes=256, format="json") + page = read_context_capture(args) + end = page["next_offset"] + assert page == {"ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, + "offset": 0, "next_offset": end, "total_bytes": len(raw), "eof": False, + "content": raw[:end].decode(), "next_command": context_capture_read_command(capture, digest, offset=end, page_bytes=256)} + args.offset, args.page_bytes = len(raw) - 2, 8192 + eof = read_context_capture(args) + # EOF has no next_command and can fit even when any continuation cannot. + args.max_page_output_bytes = len(json.dumps(eof, ensure_ascii=False, separators=(",", ":")).encode()) + 1 + assert read_context_capture(args) == eof + + +def test_bounded_continuation_rechecks_original_capture_and_utf8_cursor(tmp_path): + import argparse + from loopx.cli_commands.quota_capture import read_context_capture + capture = tmp_path / "original.json" + raw = json.dumps({"requirements": "🙂" * 900}, ensure_ascii=False).encode() + capture.write_bytes(raw) + args = argparse.Namespace(capture_file=str(capture), capture_sha256=hashlib.sha256(raw).hexdigest(), + offset=0, page_bytes=8192, max_page_output_bytes=1600, format="json") + page = read_context_capture(args) + assert page["ok"] and not page["eof"] + args.offset = page["next_offset"] + 1 + assert "UTF-8 boundary" in read_context_capture(args)["reason"] + args.offset = page["next_offset"] + capture.write_bytes(raw.replace(b"requirements", b"requirementX")) + result = read_context_capture(args) + assert not result["ok"] and "digest mismatch" in result["reason"] + assert "no guard, admission or spend" in result["recovery"] + + +def test_page_output_budget_rejects_guard_use_without_admission(tmp_path, monkeypatch): + 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"] + assert _heartbeat_receipt_count(runtime, "invalid-page-budget-guard") == 0 + assert _spend_run_count(runtime) == 0 + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("profile", ["codex_app", "generic_cli"]) def test_cli_capture_exact_delta_and_same_capture_recovery(tmp_path: Path, monkeypatch, provider, profile): From 5c490578f1830df87052c9913da27f6c1021fcc6 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:43:34 +0800 Subject: [PATCH 25/40] perf(context): calibrate native pages against batched read cost Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 10 +++++----- loopx/control_plane/turn_driver/decision_capture.py | 2 +- tests/control_plane/test_codex_context_capture.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 8390f92029..485c883f3b 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -222,7 +222,7 @@ setting. Ordinary machine validation still applies to business operations. 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 stays at 3 KiB. Continuations -request up to 8 KiB of content with `--max-page-output-bytes 7000`, which bounds +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, read one bounded output at a time. This is not a guarantee of model retention. @@ -387,8 +387,8 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 私有传输末级目录并生成已有 hook 和恢复指令,不安装或信任 hook,也不更改宿主配置。 原生 `quota should-run` 与已保存捕获的 `context-project` 共用分页呈现,直接返回第零页、 固定摘要的后续读取与完整恢复,不另行准入。内联页保持 3 KiB;后续命令最多请求 -8 KiB 正文,并用 `--max-page-output-bytes 7000` 将成功 JSON stdout(含转义、 -后续命令与换行)限制为 7000 字节。先消费内联页,再跟随 next_command;保留历史 +8 KiB 正文,并用 `--max-page-output-bytes 7900` 将成功 JSON stdout(含转义、 +后续命令与换行)限制为 7900 字节。先消费内联页,再跟随 next_command;保留历史 上限未知时,每次只读一个有界输出。该上限不证明模型保留。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 读取器校验不可变 capture 的摘要。Post-tool 适配层复用显式输入观察的字节范围校验, @@ -447,7 +447,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 to leave room for projection metadata. Subsequent native commands request up to -8 KiB of content within a 7000-byte successful JSON stdout limit. The optional +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 without a zero-progress page; increase it for the same capture. Omitting the flag @@ -470,7 +470,7 @@ input retention. 开启时,成功的已保存投影同时用现有 `context_capture_page_v1` 字段返回第一页。 该页内容最多 3 KiB,为投影元数据留出输出空间;后续原生命令最多请求 8 KiB -正文,成功 JSON stdout 限制为 7000 字节。可选的 JSON 专用参数 +正文,成功 JSON stdout 限制为 7900 字节。可选的 JSON 专用参数 `--max-page-output-bytes` 按 UTF-8 边界缩减正文,并在 `next_command` 中保留。 上限不足时返回错误,不返回无进展的分页;增大上限后重读同一捕获。省略该参数 保持旧读取输出。错误消息不受无法满足的分页上限限制,也不更改宿主或历史输出预算。 diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index 5ffa40acfd..e127e6a16b 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -136,7 +136,7 @@ def validate_handoff_socket(value: str) -> Path: # Leave room below the native 2000-token exec output boundary for framing. # This does not establish downstream retained-history capacity or retention. NATIVE_CONTEXT_PAGE_BYTES = 8192 -NATIVE_CONTEXT_PAGE_OUTPUT_BYTES = 7000 +NATIVE_CONTEXT_PAGE_OUTPUT_BYTES = 7900 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 fb919a106e..fb9fadf294 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -65,7 +65,7 @@ def _last_page(request): for item in reversed(request["input"]): output = item.get("output") parts = [p.get("text", "") for p in output] if isinstance(output, list) else [output] - for part in parts: + for part in reversed(parts): if not isinstance(part, str): continue try: @@ -246,9 +246,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 <= 7000 + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 7900 if not page["eof"]: - assert "--max-page-output-bytes 7000" in page["next_command"] + assert "--max-page-output-bytes 7900" in page["next_command"] return tool(page["next_command"], number) if stages["pages"]: cmd = stages["pages"].pop(0) From b83470d3f9a55e5b914eb239248668b17e69bd68 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:21:14 +0800 Subject: [PATCH 26/40] fix(context): qualify same-session native resume from completed reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 24 ++- .../quota/native_context_lifecycle.ts | 11 +- loopx/control_plane/quota/turn_envelope.py | 6 +- .../turn_driver/codex_context.py | 6 +- .../turn_driver/codex_context_history.py | 149 ++++++++++++++++++ .../test_codex_context_capture.py | 21 ++- .../test_codex_context_resume.py | 94 +++++++++++ .../control_plane_ts/context_delivery.test.ts | 22 +++ 8 files changed, 314 insertions(+), 19 deletions(-) create mode 100644 loopx/control_plane/turn_driver/codex_context_history.py create mode 100644 tests/control_plane/test_codex_context_resume.py diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 485c883f3b..bce564e95b 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -256,10 +256,19 @@ history. Benchmark preparation stages those same hooks in its isolated trial hom `PostToolUse` records complete digest-bound reader ranges in the existing session journal; EOF without preceding pages, a known malformed/truncated page or a reader error cannot establish a baseline. Concurrent and repeated pages coalesce. -`PreCompact`, `PostCompact`, clear, fork, a changed session/workspace and resume -invalidate it. A resume may supply different history under the same session ID; -the [native event contract](https://learn.chatgpt.com/docs/hooks#sessionstart) -does not attest retention. A new independent decision uses its normal guarded +`PreCompact`, `PostCompact`, clear, fork and a changed session/workspace invalidate +it. A resume may supply different history under the same session ID; the +[native event contract](https://learn.chatgpt.com/docs/hooks#sessionstart) alone +does not attest retention. The shared Codex adapter can qualify a resume against +the complete read's native replay checkpoint: the native-selected physical +rollout must retain its digest-bound prefix and exact last reader output, with +matching session, workspace, model and permission mode, and no intervening +compaction, rollback, replacement or unknown replay record. This bounded IO +profile targets Codex 0.162.1 paginated rollouts; missing, changed, +oversized (over 64 MiB) or unqualified sources fall back to full. It retains the +normal-read engineering assumption rather than certifying future model input; +it neither scans model requests nor asks the model for an ACK. +A new independent decision uses its normal guarded context command and reads the resulting current full capture; it need not first read an older capture to seed a delta. Continuing a captured decision after context loss still requires complete recovery of that exact original, never a new guard @@ -407,8 +416,13 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 `PostToolUse` 在现有 session journal 中合并摘要固定的完整读取范围;缺少前页的 EOF、 已知的格式损坏/截断或读取失败不能建立基线。并发、重复页幂等合并。`PreCompact`、 -`PostCompact`、清空、分叉、会话/工作区切换及 resume 使基线失效;同一会话 ID 的 resume +`PostCompact`、清空、分叉、会话/工作区切换使基线失效;同一会话 ID 的 resume 也可能使用不同历史,[原生事件契约](https://learn.chatgpt.com/docs/hooks#sessionstart)本身不证明保留。 +共享 Codex adapter 可核对完整读取时的原生重放检查点:原生实际选择的物理历史文件, +须保留摘要固定的前缀及精确的最后一份 reader 输出;会话、工作区、模型和权限模式相同, +中间没有压缩、回滚、替换或未知重放记录。此有界 IO profile 面向 Codex 0.162.1 的分页 +rollout;来源缺失、变化、超过 64 MiB 或无法资格化时回退全量。仍采用正常读取后的 +工程假设,不声称证明未来模型输入;不扫描模型请求,也不要求模型 ACK。 新的独立决定走正常准入与上下文命令,完整读取其返回的当前全量捕获;不必为了建立差分 基线而先读旧捕获。若丢失上下文后继续某个已捕获决定的依赖工作,仍须完整恢复那份精确 原捕获,不能用新 guard 替代。两条路径都只在完整且已校验的全量读取后建立新差分链。 diff --git a/loopx/control_plane/quota/native_context_lifecycle.ts b/loopx/control_plane/quota/native_context_lifecycle.ts index 863b2797ec..d8d3d50cd8 100644 --- a/loopx/control_plane/quota/native_context_lifecycle.ts +++ b/loopx/control_plane/quota/native_context_lifecycle.ts @@ -46,10 +46,13 @@ export function updateNativeContextLifecycle(request: JsonObject): JsonObject { delete state.active_call; // retire legacy diagnostic-call binding }; if (name === "SessionStart") { - // A resume may supply different history under the same session id. The - // event carries no retained-context proof. A fresh decision starts with a - // current full capture; dependent continuation recovers its pinned original - // before establishing a new chain. Ordinary tools/turn endings stay warm. + // The provider may qualify the native-selected replay source against the + // completed read checkpoint. Neither an event's same ID nor a disk journal + // is that observation. Unknown/replaced/truncated histories still reset. + const candidate = jsonObject(state.candidate); + if (same && event.source === "resume" && request.resume_verified === true + && !state.pending && candidate?.basis === NATIVE_CONTEXT_BASIS + && candidate.capture_bytes_verified === true) return state; invalidate(); return state; } diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 84e6b79ba7..4179d8b806 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -138,10 +138,12 @@ def prepare_context_delivery( def update_native_context_lifecycle(state: Mapping[str, Any], event: Mapping[str, Any], *, new_epoch: str, pages: list[dict[str, Any]], - pages_verified: bool = False) -> dict[str, Any]: + pages_verified: bool = False, + resume_verified: bool = False) -> dict[str, Any]: """Native IO adapter; the shared typed owner reduces continuity assumptions.""" return _turn_envelope_result("native_context_lifecycle", state=dict(state), - event=dict(event), new_epoch=new_epoch, pages=pages, pages_verified=pages_verified) + event=dict(event), new_epoch=new_epoch, pages=pages, pages_verified=pages_verified, + resume_verified=resume_verified) def restore_context_delivery( diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 6bbfd3e2ee..f05fab21b7 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -272,6 +272,7 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str """ from ..quota.turn_envelope import update_native_context_lifecycle from ..effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError + from .codex_context_history import checkpoint_native_read, native_resume_verified if directory is None: return {} @@ -294,7 +295,10 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str if name == "PostToolUse": pages = _verified_context_pages(state, pages) updated = update_native_context_lifecycle(state, event, - new_epoch=uuid.uuid4().hex, pages=pages, pages_verified=True) + new_epoch=uuid.uuid4().hex, pages=pages, pages_verified=True, + resume_verified=name == "SessionStart" and native_resume_verified(state, event)) + if name == "PostToolUse": + checkpoint_native_read(state, updated, event, pages) if updated != state: _save(root, updated) recovery_needed = same and not updated.get("candidate") diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py new file mode 100644 index 0000000000..187a660bc6 --- /dev/null +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -0,0 +1,149 @@ +"""Bounded IO observations for native-selected Codex replay, never Turn authority. + +SessionStart(resume) is emitted after native history reconstruction. Reuse still +assumes the qualified native host replays its selected append-only source. Bind +that source to an observed complete read, require its unchanged physical prefix +and exact persisted last reader output, and reject unknown/lossy replay tails. +This is not a scan of model inputs or a certificate from a session ID/journal. +""" +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import re +import stat +from typing import Any + +# A compatibility profile of the native rollout, not a shared control vocabulary. +# Unknown record types fall back to full until their replay semantics qualify. +_APPEND_RECORDS = {"response_item", "turn_context", "world_state", "token_usage_record"} +_APPEND_EVENTS = {"task_started", "task_complete", "item_completed", "token_count", + "thread_settings_applied", "exec_command_begin", "exec_command_end", "agent_message", + "agent_reasoning", "agent_reasoning_raw_content", "agent_reasoning_section_break", + "agent_reasoning_raw_content_delta", "agent_reasoning_delta", "agent_message_delta", + "hook_started", "hook_completed", "warning", "error", "stream_error", "turn_aborted", + "terminal_interaction", "background_event"} +_MAX_HISTORY_BYTES = 64 * 1024 * 1024 +_SHELL_OUTPUT = re.compile(r"\AChunk ID: [^\r\n]+\nWall time: [0-9.]+ seconds\n" + r"Process exited with code 0\n(?:Original token count: [0-9]+\n)?Output:\n(.*)\Z", re.S) + + +def _digest(value: object) -> str: + return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True, + separators=(",", ":")).encode()).hexdigest() + + +def _snapshot(event: dict[str, Any]) -> tuple[dict[str, Any], bytes] | None: + path = Path(event.get("transcript_path") or "") + if not path.is_absolute() or path.resolve(strict=True) != path: + return None + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + with os.fdopen(descriptor, "rb") as stream: + info = os.fstat(stream.fileno()) + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_ino != path.lstat().st_ino + or info.st_mode & 0o022 or not 0 < info.st_size <= _MAX_HISTORY_BYTES): + return None + raw = stream.read(info.st_size + 1) + after = os.fstat(stream.fileno()) + if len(raw) != info.st_size or after.st_size != info.st_size or not raw.endswith(b"\n"): + return None + first = json.loads(raw.split(b"\n", 1)[0]) + meta = first.get("payload", {}) + if (first.get("type") != "session_meta" or meta.get("id") != event.get("session_id") + or meta.get("cwd") != event.get("cwd") or meta.get("forked_from_id") + or meta.get("history_mode") != "paginated" or meta.get("cli_version") != "0.162.1" + or not event.get("model") or not event.get("permission_mode")): + return None + return {"path": str(path), "device": info.st_dev, "inode": info.st_ino, + "prefix_bytes": len(raw), "prefix_sha256": hashlib.sha256(raw).hexdigest(), + "model": event["model"], "permission_mode": event["permission_mode"]}, raw + + +def _read_pages(output: object) -> list[dict[str, Any]]: + from .codex_context import _context_pages + + # This is the native exec_command result envelope, not a prose classifier. + if isinstance(output, str) and (match := _SHELL_OUTPUT.fullmatch(output)): + output = match[1] + return _context_pages(output) + + +def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, + event: dict[str, Any]) -> bool: + if any(anchor.get(k) != snapshot.get(k) for k in + ("path", "device", "inode", "model", "permission_mode")): + return False + end = anchor.get("prefix_bytes") + if (type(end) is not int or not 0 < end <= len(raw) or raw[end - 1:end] != b"\n" + or hashlib.sha256(raw[:end]).hexdigest() != anchor.get("prefix_sha256")): + return False + # A native writer may persist the last output before or after PostToolUse. + # Require its exact page bytes once in the selected history in either case. + matches = 0 + for line in raw.splitlines(): + record = json.loads(line) + payload = record.get("payload", {}) + if (record.get("type") == "response_item" and payload.get("type") == "function_call_output" + and payload.get("call_id") == anchor.get("read_call_id")): + if _digest(_read_pages(payload.get("output"))) != anchor.get("read_pages_sha256"): + return False + matches += 1 + if matches != 1: + return False + for line in raw[end:].splitlines(): + record = json.loads(line) + kind, payload = record.get("type"), record.get("payload", {}) + if kind == "event_msg": + if payload.get("type") not in _APPEND_EVENTS: + return False + # Changes to history mode are not ordinary append-only settings. + if payload.get("type") == "thread_settings_applied" and payload.get("history_mode") not in (None, "paginated"): + return False + elif kind not in _APPEND_RECORDS: + return False # includes compaction, retained-history replacement and rollback + if kind == "turn_context" and (payload.get("cwd") != event["cwd"] or payload.get("model") != event["model"]): + return False + return True + + +def native_resume_verified(state: dict[str, Any], event: dict[str, Any]) -> bool: + """Observe a qualified replay source; the shared TS owner decides reuse.""" + candidate = state.get("candidate") or {} + anchor = candidate.get("native_resume") + if (event.get("source") != "resume" or state.get("pending") or not isinstance(anchor, dict) + or state.get("session_id") != event.get("session_id") or state.get("cwd") != event.get("cwd")): + return False + try: + snapshot = _snapshot(event) + return snapshot is not None and _matches(anchor, *snapshot, event) + except (OSError, ValueError, TypeError, KeyError, AttributeError, RecursionError): + return False + + +def checkpoint_native_read(state: dict[str, Any], updated: dict[str, Any], + event: dict[str, Any], pages: list[dict[str, Any]]) -> None: + """Attach content-free native source facts only after the typed complete read. + + A delta checkpoint can advance only a previously verified replay chain. + Failure affects cross-process reuse, not normal in-process reader delivery. + """ + candidate = updated.get("candidate") + if (not isinstance(candidate, dict) or updated.get("pending") or not pages + or candidate == state.get("candidate") or not event.get("tool_use_id")): + return + try: + snapshot = _snapshot(event) + if snapshot is None: + return + facts, raw = snapshot + if candidate.get("ancestry", 0): + anchor = (state.get("candidate") or {}).get("native_resume") + if not isinstance(anchor, dict) or not _matches(anchor, facts, raw, event): + return + candidate["native_resume"] = {**facts, "read_call_id": event["tool_use_id"], + "read_pages_sha256": _digest(pages)} + except (OSError, ValueError, TypeError, KeyError, AttributeError, RecursionError): + return diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index fb9fadf294..9406b3fd64 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -270,13 +270,20 @@ def response(request, number, message): stages["phase"] = name + "_ready" if phase == "resuming": assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") - assert "LoopX context baseline invalidated." in _input_text(request) - assert wires["cold"]["recovery"]["command"] in _input_text(request) - # Resume itself does not attest history retention. Read the - # same original through the real CLI before the next guard. - stages["phase"] = "resume_read" - stages["pages"] = pages("cold", read_paths["cold"]) - cmd = stages["pages"].pop(0) + if host == "benchmark": + # Actual worker process exit/resume, qualified by the shared + # native-selected replay checkpoint. No old full prefetch. + assert "LoopX context baseline invalidated." not in _input_text(request) + stages["phase"] = "warm_ready" + cmd = invoke("warm") + else: + assert "LoopX context baseline invalidated." in _input_text(request) + assert wires["cold"]["recovery"]["command"] in _input_text(request) + # Unqualified native histories still recover the exact + # original before continuing its dependent decision. + stages["phase"] = "resume_read" + stages["pages"] = pages("cold", read_paths["cold"]) + cmd = stages["pages"].pop(0) else: cmd = invoke(name) elif phase == "resume_read": diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py new file mode 100644 index 0000000000..f4927a5757 --- /dev/null +++ b/tests/control_plane/test_codex_context_resume.py @@ -0,0 +1,94 @@ +"""Native replay continuity: completed deliveries, real history changes, fallback.""" +from __future__ import annotations + +import json +import pytest + +from loopx.control_plane.turn_driver import codex_context as context +from tests.control_plane.test_codex_context_lifecycle import setup, project, read_pages + + +def append(path, kind, payload): + with path.open("a") as stream: + stream.write(json.dumps({"type": kind, "payload": payload}) + "\n") + + +def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False): + event = setup(root, monkeypatch) + history = root / "rollout.jsonl" + append(history, "session_meta", {"id": "session", "cwd": str(root), + "cli_version": "0.162.1", "history_mode": "paginated"}) + event.update(transcript_path=str(history), model="fixture", permission_mode="bypassPermissions") + append(history, "turn_context", {"cwd": str(root), "model": "fixture"}) + payload = {"ok": True, "plan_summary": {"requirements": "Keep original evidence. " * 1000}, + "heartbeat_receipt": {"turn_instance_id": "first"}} + _, first = project(root, payload=payload) + pages = read_pages(root, first) + for i, page in enumerate(pages if complete else pages[:-1]): + call = f"reader-{i}" + body = json.dumps(page) + output = ("Chunk ID: fixture\nWall time: 0.01 seconds\nProcess exited with code 0\n" + "Original token count: 1000\nOutput:\n" + body) if shell_wrapper else body + append(history, "response_item", {"type": "function_call_output", "call_id": call, "output": output}) + context.codex_context_hook(str(root), {**event, "hook_event_name": "PostToolUse", + "tool_use_id": call, "tool_response": body}) + return event, history, payload + + +@pytest.mark.parametrize("shell_wrapper", [False, True]) +def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_path, monkeypatch, shell_wrapper): + event, history, payload = observed_read(tmp_path, monkeypatch, shell_wrapper=shell_wrapper) + before = context._state(tmp_path) + append(history, "event_msg", {"type": "task_complete"}) + append(history, "event_msg", {"type": "thread_settings_applied", "history_mode": "paginated"}) + result = context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + # No old recovery instruction or extra reader call is needed. + assert result == {} and context._state(tmp_path)["context_epoch"] == before["context_epoch"] + current = {**payload, "heartbeat_receipt": {"turn_instance_id": "second"}} + _, wire = project(tmp_path, "second", payload=current) + assert wire["mode"] == "delta" + assert wire["changes"]["values"]["heartbeat_receipt"] == current["heartbeat_receipt"] + + +@pytest.mark.parametrize("change", ["compact", "rollback", "retained", "unknown", "truncate", + "replace", "rewrite", "missing", "output", "duplicate", "model", "workspace", "session", "permission"]) +def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, monkeypatch, change): + event, history, payload = observed_read(tmp_path, monkeypatch) + before = context._state(tmp_path) + if change in {"compact", "retained", "unknown"}: + append(history, {"compact": "compacted", "retained": "retained_context", "unknown": "future_history"}[change], {}) + elif change == "rollback": + append(history, "event_msg", {"type": "thread_rolled_back", "num_turns": 1}) + elif change == "truncate": + history.write_bytes(history.read_bytes()[:100]) + elif change == "replace": + replacement = tmp_path / "replacement.jsonl" + replacement.write_bytes(history.read_bytes()) + replacement.replace(history) + elif change == "rewrite": + history.write_bytes(history.read_bytes().replace(b"Keep original", b"Drop original")) + elif change == "missing": + history.unlink() + elif change in {"output", "duplicate"}: + rows = [json.loads(line) for line in history.read_text().splitlines()] + if change == "output": + rows[-1]["payload"]["output"] = "truncated" + else: + rows.append(rows[-1]) + history.write_text("".join(json.dumps(row) + "\n" for row in rows)) + else: + event[{"workspace": "cwd", "session": "session_id", "permission": "permission_mode"}.get(change, change)] = "different" + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + after = context._state(tmp_path) + assert "candidate" not in after and after["context_epoch"] != before["context_epoch"] + if change not in {"session", "workspace"}: + assert after["recovery"] == before["recovery"] + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == "full" and wire["context"] == payload + + +def test_incomplete_native_read_cannot_gain_resume_continuity(tmp_path, monkeypatch): + event, _, payload = observed_read(tmp_path, monkeypatch, complete=False) + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == "full" and wire["context"] == payload diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index 69352cb449..6558a3c785 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -4,6 +4,28 @@ import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; import {compactTodoContextSource, prepareContextDelivery, restoreContextDelivery} from "../../loopx/control_plane/quota/context_delivery.ts"; import {evaluateTurnEnvelope} from "../../loopx/control_plane/quota/turn_envelope.ts"; +import {updateNativeContextLifecycle} from "../../loopx/control_plane/quota/native_context_lifecycle.ts"; + +test("only the provider-qualified native replay can retain a complete resume baseline", () => { + const state = {session_id: "native", cwd: "/workspace", context_epoch: "original", + candidate: {basis: "native_hook_assumption", capture_bytes_verified: true}}; + const event = {hook_event_name: "SessionStart", source: "resume", session_id: "native", cwd: "/workspace"}; + const request = {state, event, new_epoch: "replacement", resume_verified: true}; + assert.deepEqual(updateNativeContextLifecycle(request), state); + for (const invalid of [ + {...request, resume_verified: false}, + {...request, event: {...event, resume_verified: true}, resume_verified: false}, + {...request, state: {...state, pending: {}}}, + {...request, state: {...state, candidate: {...state.candidate, capture_bytes_verified: false}}}, + {...request, event: {...event, source: "compact"}}, + {...request, event: {...event, cwd: "/different"}}, + ]) { + const result = updateNativeContextLifecycle(invalid); + assert.equal(result.context_epoch, "replacement"); + assert.equal(result.candidate, undefined); + } + assert.equal(state.context_epoch, "original"); +}); test("replacements and sparse edits own their values without mutating either capture", () => { const original = {registry: {body: "Retain every original requirement. ".repeat(400), changed: {value: 1}}, From 7af81167d4550e14a0b38606f9e47423092fc5e4 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:34:11 +0800 Subject: [PATCH 27/40] fix(benchmark): create private native history for opted-in resume Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/runtime/worker.py | 3 + benchmark/tests/test_shared_codex_runtime.py | 18 ++++++ docs/reference/required-work-context.md | 5 ++ .../test_codex_context_capture.py | 60 +++++++++++++++---- .../test_codex_context_resume.py | 6 +- 5 files changed, 81 insertions(+), 11 deletions(-) diff --git a/benchmark/runtime/worker.py b/benchmark/runtime/worker.py index 8425ab97fe..c0e63f8668 100644 --- a/benchmark/runtime/worker.py +++ b/benchmark/runtime/worker.py @@ -150,6 +150,9 @@ def child_process(command: list[str], *, env: dict[str, str], stdout, stderr, stderr=stderr, text=True, start_new_session=True, + # Opted-in replay checkpoints require owner-only native writes. + # This affects the isolated child, never the parent or off mode. + umask=0o077 if os.name == "posix" and env.get("LOOPX_CONTEXT_DELIVERY") == "on" else -1, ) yield process finally: diff --git a/benchmark/tests/test_shared_codex_runtime.py b/benchmark/tests/test_shared_codex_runtime.py index cba544c5e8..2b1882d6d4 100644 --- a/benchmark/tests/test_shared_codex_runtime.py +++ b/benchmark/tests/test_shared_codex_runtime.py @@ -1134,6 +1134,24 @@ def unexpected(*args): assert not (Path(env["LOOPX_CODEX_HOME"]) / "hooks.json").exists() +@pytest.mark.parametrize("setting,mode", [("off", 0o664), ("on", 0o600)]) +@pytest.mark.skipif(os.name != "posix", reason="Native POSIX file creation permissions") +def test_context_native_writer_is_private_without_changing_off_umask(tmp_path, setting, mode): + from benchmark.runtime import worker + + original = os.umask(0o002) + try: + command = [sys.executable, "-c", "from pathlib import Path; Path('native-history').write_bytes(b'fixture')"] + with worker.child_process(command, env={**os.environ, "LOOPX_PROJECT": str(tmp_path), + "LOOPX_CONTEXT_DELIVERY": setting}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as child: + child.communicate(timeout=10) + assert child.returncode == 0 + assert (tmp_path / "native-history").stat().st_mode & 0o777 == mode + assert os.umask(0o002) == 0o002 + finally: + os.umask(original) + + def test_context_selection_is_fixed_by_trial_home(tmp_path): args = settings(tmp_path, execution=Execution(mode="heartbeat", context_delivery="on")) prepare_codex_home(**args) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index bce564e95b..1a22efeec5 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -268,6 +268,9 @@ profile targets Codex 0.162.1 paginated rollouts; missing, changed, oversized (over 64 MiB) or unqualified sources fall back to full. It retains the normal-read engineering assumption rather than certifying future model input; it neither scans model requests nor asks the model for an ACK. +Opted-in benchmark native children create owner-only files automatically; the +parent process and off-mode file creation policy stay unchanged. Group-writable +existing replay sources cannot establish cross-process continuity. A new independent decision uses its normal guarded context command and reads the resulting current full capture; it need not first read an older capture to seed a delta. Continuing a captured decision after context @@ -423,6 +426,8 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 中间没有压缩、回滚、替换或未知重放记录。此有界 IO profile 面向 Codex 0.162.1 的分页 rollout;来源缺失、变化、超过 64 MiB 或无法资格化时回退全量。仍采用正常读取后的 工程假设,不声称证明未来模型输入;不扫描模型请求,也不要求模型 ACK。 +开启后的 benchmark 原生子进程自动以仅所有者可访问的权限创建文件,父进程与关闭态 +保持原权限策略;既有可组写的重放来源不能建立跨进程连续性。 新的独立决定走正常准入与上下文命令,完整读取其返回的当前全量捕获;不必为了建立差分 基线而先读旧捕获。若丢失上下文后继续某个已捕获决定的依赖工作,仍须完整恢复那份精确 原捕获,不能用新 guard 替代。两条路径都只在完整且已校验的全量读取后建立新差分链。 diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 9406b3fd64..31cc3db903 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -87,7 +87,7 @@ def _last_page(request): + [(host, surface, False, True, False) for host in ("benchmark", "code_mode") for surface in ("quota_packet", "turn_envelope")] + [("code_mode", surface, False, True, True) for surface in ("quota_packet", "turn_envelope")]) -def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, bounded_pages=False): +def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False): isolate_sqlite_runtime(tmp_path, monkeypatch) binary, env, workspace, home = _native_fixture(tmp_path) env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) @@ -269,11 +269,12 @@ def response(request, number, message): name = "cold" if phase == "start" else "warm" stages["phase"] = name + "_ready" if phase == "resuming": - assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") + if not resume_loss: + assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") if host == "benchmark": # Actual worker process exit/resume, qualified by the shared # native-selected replay checkpoint. No old full prefetch. - assert "LoopX context baseline invalidated." not in _input_text(request) + assert ("LoopX context baseline invalidated." in _input_text(request)) == bool(resume_loss) stages["phase"] = "warm_ready" cmd = invoke("warm") else: @@ -298,13 +299,13 @@ def response(request, number, message): # Keep the independent full-wire oracle separate from its page view. wire_file = Path(descriptor["path"]).parent / "delivery.json" wires[name] = json.loads(wire_file.read_text()) - assert descriptor["mode"] == wires[name]["mode"] == ("full" if name == "cold" else "delta") + assert descriptor["mode"] == wires[name]["mode"] == ("full" if name == "cold" or resume_loss else "delta") assert "Native input observation independently verifies" not in descriptor["instruction"] - if name == "warm": + if name == "warm" and not resume_loss: assert json.dumps(wires[name]["instruction"]).encode() in descriptor["content"].encode() capture = Path(wires[name]["recovery"]["path"]).parent / "decision.json" snapshots.append((capture, capture.read_bytes())) - read_paths[name] = Path(wires[name]["recovery"]["path"]) if name == "cold" else wire_file + read_paths[name] = Path(wires[name]["recovery"]["path"]) if wires[name]["mode"] == "full" else wire_file stages["pages"] = pages(name, read_paths[name]) stages["phase"] = name + "_read" cmd = stages["pages"].pop(0) @@ -318,7 +319,8 @@ def response(request, number, message): return message cmd = invoke("warm") elif phase == "warm_read": - assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") + if not resume_loss: + assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") assert delivered(request, "warm") == read_paths["warm"].read_text().rstrip("\n") stages.update(phase="compact", compact_response=number) if hook_loss: @@ -506,6 +508,20 @@ def held(*args, **kwargs): rc, spent = _run_cli(registry, runtime, "quota", "spend-slot", *binding, "--slots", "1", "--source", "heartbeat", "--execute", cwd=project) assert rc == 0 and spent["appended"], spent + if resume_loss: + journal = json.loads((home / "loopx-context/session.json").read_text()) + native_history = Path(journal["candidate"]["native_resume"]["path"]) + if resume_loss == "replace": + replacement = native_history.with_suffix(".replacement") + replacement.write_bytes(native_history.read_bytes()) + replacement.chmod(native_history.stat().st_mode & 0o777) + replacement.replace(native_history) + else: + rows = [json.loads(line) for line in native_history.read_text().splitlines()] + # The native provider accepts this source metadata, but + # the LoopX replay profile must fail closed on it. + rows[0]["payload"]["cli_version"] = "unqualified-fixture-host" + native_history.write_text("".join(json.dumps(row) + "\n" for row in rows)) result = run_once(env) assert result["ok"] and result["host_invoked"], result assert result["context_transport"] == {"available": True} @@ -531,7 +547,8 @@ def held(*args, **kwargs): assert len(captured) == 2 # Compare complete presentation bytes, not two similarly sized first-page descriptors. # Whole CLI/model costs remain a separate benchmark measurement. - assert len(json.dumps(wires["warm"]).encode()) < len(json.dumps(wires["cold"]).encode()) + if not resume_loss: + assert len(json.dumps(wires["warm"]).encode()) < len(json.dumps(wires["cold"]).encode()) assert len(calls) == len(observed) and stages["baseline_confirmed"] if native_read_batch > 2: visible_batches = [] @@ -556,7 +573,8 @@ def held(*args, **kwargs): assert hashlib.sha256(projection.read_bytes()).hexdigest() == wires["warm"]["recovery"]["sha256"] current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, capture_id=wires["warm"]["target"]["capture_id"], surface=surface) - assert restore_context_delivery(wires["warm"], baseline=wires["cold"], current=current)["context"] == current["context"] + restored = restore_context_delivery(wires["warm"], baseline=wires["cold"], current=current) + assert restored["context"] == current["context"] and restored["mode"] == wires["warm"]["mode"] for document in (wires["cold"]["context"], current["context"]): context = (document["work_context"] if surface == "turn_envelope" else document["interaction_contract"]["agent_channel"]["work_context"]) @@ -572,7 +590,22 @@ def held(*args, **kwargs): actions = json.loads(raw)["interaction_contract"]["cli_channel"]["next_cli_actions"] assert actions and all(shlex.split(action)[0] == expected_cli for action in actions) if host == "benchmark" and resumed: - assert _spend_run_count(runtime) == 1 + # Both current heartbeat decisions settle exactly once. Compaction and + # recovery may not introduce a third guard or spend. + warm_turn = json.loads(snapshots[1][1])["heartbeat_receipt"]["turn_instance_id"] + assert warm_turn != cold_turn + artifact.write_text(hashlib.sha256(read_paths["warm"].read_bytes()).hexdigest()) + binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", warm_turn) + rc, written = _run_cli(registry, runtime, "refresh-state", *binding, + "--classification", "native_resume_readback_verified", "--delivery-batch-scale", "single_surface", + "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--no-global-sync", "--suppress-external-sinks", cwd=project) + assert rc == 0, written + rc, spent = _run_cli(registry, runtime, "quota", "spend-slot", *binding, + "--slots", "1", "--source", "heartbeat", "--execute", cwd=project) + assert rc == 0 and spent["appended"], spent + assert _spend_run_count(runtime) == 2 from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted retained = next(todo for todo in read_canonical_todos_if_promoted( runtime_root=runtime, goal_id=GOAL_ID)["todos"] if todo["todo_id"] == TODO_ID) @@ -583,6 +616,13 @@ def held(*args, **kwargs): assert bounded_sizes and max(bounded_sizes) > 3072 +@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): + test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, + "benchmark", surface, False, True, False, resume_loss=loss) + + @pytest.mark.parametrize("host,surface,hook_loss", [ (host, surface, loss) for host in ("code_mode", "benchmark") for surface, loss in (("quota_packet", "missing"), ("turn_envelope", "killed")) diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index f4927a5757..1cdd195384 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -18,6 +18,7 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False): history = root / "rollout.jsonl" append(history, "session_meta", {"id": "session", "cwd": str(root), "cli_version": "0.162.1", "history_mode": "paginated"}) + history.chmod(0o600) event.update(transcript_path=str(history), model="fixture", permission_mode="bypassPermissions") append(history, "turn_context", {"cwd": str(root), "model": "fixture"}) payload = {"ok": True, "plan_summary": {"requirements": "Keep original evidence. " * 1000}, @@ -51,7 +52,7 @@ def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_pa @pytest.mark.parametrize("change", ["compact", "rollback", "retained", "unknown", "truncate", - "replace", "rewrite", "missing", "output", "duplicate", "model", "workspace", "session", "permission"]) + "replace", "rewrite", "missing", "output", "duplicate", "model", "workspace", "session", "permission", "writable"]) def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, monkeypatch, change): event, history, payload = observed_read(tmp_path, monkeypatch) before = context._state(tmp_path) @@ -64,11 +65,14 @@ def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, elif change == "replace": replacement = tmp_path / "replacement.jsonl" replacement.write_bytes(history.read_bytes()) + replacement.chmod(0o600) replacement.replace(history) elif change == "rewrite": history.write_bytes(history.read_bytes().replace(b"Keep original", b"Drop original")) elif change == "missing": history.unlink() + elif change == "writable": + history.chmod(0o664) elif change in {"output", "duplicate"}: rows = [json.loads(line) for line in history.read_text().splitlines()] if change == "output": From 10a44009e274c3b4f9ba0f4f8bc5b88092a75bf9 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:04:28 +0800 Subject: [PATCH 28/40] test(context): verify resumed baselines without masking full recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../turn_driver/codex_context_history.py | 2 ++ tests/control_plane/test_codex_context_capture.py | 12 +++++++++++- tests/control_plane/test_codex_context_resume.py | 4 +++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py index 187a660bc6..eec0dc5128 100644 --- a/loopx/control_plane/turn_driver/codex_context_history.py +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -112,6 +112,8 @@ def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, def native_resume_verified(state: dict[str, Any], event: dict[str, Any]) -> bool: """Observe a qualified replay source; the shared TS owner decides reuse.""" candidate = state.get("candidate") or {} + if not isinstance(candidate, dict): + return False anchor = candidate.get("native_resume") if (event.get("source") != "resume" or state.get("pending") or not isinstance(anchor, dict) or state.get("session_id") != event.get("session_id") or state.get("cwd") != event.get("cwd")): diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 31cc3db903..a70721af94 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -573,7 +573,17 @@ def held(*args, **kwargs): assert hashlib.sha256(projection.read_bytes()).hexdigest() == wires["warm"]["recovery"]["sha256"] current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, capture_id=wires["warm"]["target"]["capture_id"], surface=surface) - restored = restore_context_delivery(wires["warm"], baseline=wires["cold"], current=current) + baseline = wires["cold"] + if resumed and host != "benchmark": + # This path independently reread every original byte after a reset. + # Bind those verified bytes to the new native epoch, rather than letting + # automatic full fallback conceal an obsolete pre-loss baseline. + assert current["target"]["context_epoch"] != baseline["target"]["context_epoch"] + baseline = prepare_context_delivery(baseline["context"], scope={ + k: current["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, + capture_id=baseline["target"]["capture_id"], surface=surface) + assert baseline["target"] == wires["warm"]["base"] + restored = restore_context_delivery(wires["warm"], baseline=baseline, current=current) assert restored["context"] == current["context"] and restored["mode"] == wires["warm"]["mode"] for document in (wires["cold"]["context"], current["context"]): context = (document["work_context"] if surface == "turn_envelope" else diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index 1cdd195384..72c3405011 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -52,7 +52,7 @@ def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_pa @pytest.mark.parametrize("change", ["compact", "rollback", "retained", "unknown", "truncate", - "replace", "rewrite", "missing", "output", "duplicate", "model", "workspace", "session", "permission", "writable"]) + "replace", "rewrite", "missing", "output", "duplicate", "model", "workspace", "session", "permission", "writable", "malformed"]) def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, monkeypatch, change): event, history, payload = observed_read(tmp_path, monkeypatch) before = context._state(tmp_path) @@ -73,6 +73,8 @@ def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, history.unlink() elif change == "writable": history.chmod(0o664) + elif change == "malformed": + context._save(tmp_path, {**before, "candidate": "corrupt"}) elif change in {"output", "duplicate"}: rows = [json.loads(line) for line in history.read_text().splitlines()] if change == "output": From bd4a5c70cd8ee0638ae4bf03c428b0a73c571244 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:16:28 +0800 Subject: [PATCH 29/40] fix(context): preserve opaque native replay file identities Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> (cherry picked from commit c88c15f529789e750b3e08f612352034a06b9149) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/turn_driver/codex_context_history.py | 4 +++- tests/control_plane/test_codex_context_resume.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py index eec0dc5128..fcd459d3cc 100644 --- a/loopx/control_plane/turn_driver/codex_context_history.py +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -57,7 +57,9 @@ def _snapshot(event: dict[str, Any]) -> tuple[dict[str, Any], bytes] | None: or meta.get("history_mode") != "paginated" or meta.get("cli_version") != "0.162.1" or not event.get("model") or not event.get("permission_mode")): return None - return {"path": str(path), "device": info.st_dev, "inode": info.st_ino, + # Filesystem identities are opaque; large inode numbers must survive the + # shared TypeScript JSON boundary without IEEE-754 integer rounding. + return {"path": str(path), "device": str(info.st_dev), "inode": str(info.st_ino), "prefix_bytes": len(raw), "prefix_sha256": hashlib.sha256(raw).hexdigest(), "model": event["model"], "permission_mode": event["permission_mode"]}, raw diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index 72c3405011..cbc43861a5 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -40,6 +40,7 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False): def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_path, monkeypatch, shell_wrapper): event, history, payload = observed_read(tmp_path, monkeypatch, shell_wrapper=shell_wrapper) before = context._state(tmp_path) + assert before["candidate"]["native_resume"]["inode"].isdigit() append(history, "event_msg", {"type": "task_complete"}) append(history, "event_msg", {"type": "thread_settings_applied", "history_mode": "paginated"}) result = context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) From a803258c05a5840d6129b6f0be3802b4b98ab0b8 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:33:51 +0800 Subject: [PATCH 30/40] fix(context): require complete reader coverage in resumed native replay Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> (cherry picked from commit 5d1e2d2ef8361e58c8774c5a606c4d1a68d1f22f) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 6 +- .../turn_driver/codex_context.py | 8 ++- .../turn_driver/codex_context_history.py | 64 ++++++++++++++++--- .../test_codex_context_resume.py | 13 +++- 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 1a22efeec5..5f6eac2fdb 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -261,7 +261,8 @@ it. A resume may supply different history under the same session ID; the [native event contract](https://learn.chatgpt.com/docs/hooks#sessionstart) alone does not attest retention. The shared Codex adapter can qualify a resume against the complete read's native replay checkpoint: the native-selected physical -rollout must retain its digest-bound prefix and exact last reader output, with +rollout must retain its digest-bound prefix and every observed reader output, +including complete byte coverage of the pinned capture, with matching session, workspace, model and permission mode, and no intervening compaction, rollback, replacement or unknown replay record. This bounded IO profile targets Codex 0.162.1 paginated rollouts; missing, changed, @@ -422,7 +423,8 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 `PostCompact`、清空、分叉、会话/工作区切换使基线失效;同一会话 ID 的 resume 也可能使用不同历史,[原生事件契约](https://learn.chatgpt.com/docs/hooks#sessionstart)本身不证明保留。 共享 Codex adapter 可核对完整读取时的原生重放检查点:原生实际选择的物理历史文件, -须保留摘要固定的前缀及精确的最后一份 reader 输出;会话、工作区、模型和权限模式相同, +须保留摘要固定的前缀及每份精确 reader 输出,完整覆盖固定捕获的全部字节; +会话、工作区、模型和权限模式相同, 中间没有压缩、回滚、替换或未知重放记录。此有界 IO profile 面向 Codex 0.162.1 的分页 rollout;来源缺失、变化、超过 64 MiB 或无法资格化时回退全量。仍采用正常读取后的 工程假设,不声称证明未来模型输入;不扫描模型请求,也不要求模型 ACK。 diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index f05fab21b7..3887dd8726 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -272,7 +272,7 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str """ from ..quota.turn_envelope import update_native_context_lifecycle from ..effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError - from .codex_context_history import checkpoint_native_read, native_resume_verified + from .codex_context_history import checkpoint_native_read, native_resume_verified, observe_native_read if directory is None: return {} @@ -289,16 +289,18 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str with _locked(directory) as root: try: state = _state(root) + read_state = state same = state.get("session_id") == session and state.get("cwd") == event.get("cwd") if same: recovery = state.get("recovery") if name == "PostToolUse": pages = _verified_context_pages(state, pages) - updated = update_native_context_lifecycle(state, event, + read_state = observe_native_read(state, event, pages) + updated = update_native_context_lifecycle(read_state, event, new_epoch=uuid.uuid4().hex, pages=pages, pages_verified=True, resume_verified=name == "SessionStart" and native_resume_verified(state, event)) if name == "PostToolUse": - checkpoint_native_read(state, updated, event, pages) + checkpoint_native_read(read_state, updated, event, pages) if updated != state: _save(root, updated) recovery_needed = same and not updated.get("candidate") diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py index fcd459d3cc..834a3a2f69 100644 --- a/loopx/control_plane/turn_driver/codex_context_history.py +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -75,6 +75,8 @@ def _read_pages(output: object) -> list[dict[str, Any]]: def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, event: dict[str, Any]) -> bool: + from .decision_capture import capture_page_range, read_context_capture_file + if any(anchor.get(k) != snapshot.get(k) for k in ("path", "device", "inode", "model", "permission_mode")): return False @@ -82,18 +84,40 @@ def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, if (type(end) is not int or not 0 < end <= len(raw) or raw[end - 1:end] != b"\n" or hashlib.sha256(raw[:end]).hexdigest() != anchor.get("prefix_sha256")): return False - # A native writer may persist the last output before or after PostToolUse. - # Require its exact page bytes once in the selected history in either case. - matches = 0 + reads, reference = anchor.get("reads"), anchor.get("read_source") + if not isinstance(reads, list) or not reads or not isinstance(reference, dict): + return False + required = {read["call_id"]: read["pages_sha256"] for read in reads} + if len(required) != len(reads): + return False + _, body, _ = read_context_capture_file(reference["path"], reference["sha256"], private=True) + # Verify every observed read, not just EOF. A complete last page must never + # hide an earlier output that the native replay dropped or truncated. + seen: set[str] = set() + ranges = [] for line in raw.splitlines(): record = json.loads(line) payload = record.get("payload", {}) if (record.get("type") == "response_item" and payload.get("type") == "function_call_output" - and payload.get("call_id") == anchor.get("read_call_id")): - if _digest(_read_pages(payload.get("output"))) != anchor.get("read_pages_sha256"): + and payload.get("call_id") in required): + call = payload["call_id"] + pages = _read_pages(payload.get("output")) + if call in seen or _digest(pages) != required[call]: return False - matches += 1 - if matches != 1: + seen.add(call) + for page in pages: + if page.get("sha256") != reference["sha256"]: + continue + span = capture_page_range(page, raw=body, digest=reference["sha256"]) + if span is None: + return False + ranges.append(span) + cursor = 0 + for start, stop in sorted(ranges): + if start > cursor: + return False + cursor = max(cursor, stop) + if seen != set(required) or cursor != len(body): return False for line in raw[end:].splitlines(): record = json.loads(line) @@ -111,6 +135,23 @@ def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, return True +def observe_native_read(state: dict[str, Any], event: dict[str, Any], + pages: list[dict[str, Any]]) -> dict[str, Any]: + """Collect content-free IO facts alongside the existing pending byte ranges.""" + pending = state.get("pending") + call = event.get("tool_use_id") + if (not isinstance(pending, dict) or not isinstance(call, str) or not call + or not pages or any(page.get("ok") is not True for page in pages)): + return state + read = {"call_id": call, "pages_sha256": _digest(pages)} + reads = pending.get("native_reads", []) + if not isinstance(reads, list): + reads = [] + if read not in reads: + reads = [*reads, read] + return {**state, "pending": {**pending, "native_reads": reads}} + + def native_resume_verified(state: dict[str, Any], event: dict[str, Any]) -> bool: """Observe a qualified replay source; the shared TS owner decides reuse.""" candidate = state.get("candidate") or {} @@ -147,7 +188,12 @@ def checkpoint_native_read(state: dict[str, Any], updated: dict[str, Any], anchor = (state.get("candidate") or {}).get("native_resume") if not isinstance(anchor, dict) or not _matches(anchor, facts, raw, event): return - candidate["native_resume"] = {**facts, "read_call_id": event["tool_use_id"], - "read_pages_sha256": _digest(pages)} + pending = state.get("pending") or {} + reference = pending if candidate.get("ancestry", 0) else state.get("recovery") + reads = pending.get("native_reads") + if not isinstance(reference, dict) or not isinstance(reads, list) or not reads: + return + candidate["native_resume"] = {**facts, "reads": reads, + "read_source": {key: reference[key] for key in ("path", "sha256")}} except (OSError, ValueError, TypeError, KeyError, AttributeError, RecursionError): return diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index cbc43861a5..c2d6200aad 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -13,7 +13,7 @@ def append(path, kind, payload): stream.write(json.dumps({"type": kind, "payload": payload}) + "\n") -def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False): +def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost_prefix=False): event = setup(root, monkeypatch) history = root / "rollout.jsonl" append(history, "session_meta", {"id": "session", "cwd": str(root), @@ -31,6 +31,10 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False): output = ("Chunk ID: fixture\nWall time: 0.01 seconds\nProcess exited with code 0\n" "Original token count: 1000\nOutput:\n" + body) if shell_wrapper else body append(history, "response_item", {"type": "function_call_output", "call_id": call, "output": output}) + if lost_prefix and i == len(pages) - 1: + rows = [json.loads(line) for line in history.read_text().splitlines()] + next(row for row in rows if row["type"] == "response_item")["payload"]["output"] = "truncated" + history.write_text("".join(json.dumps(row) + "\n" for row in rows)) context.codex_context_hook(str(root), {**event, "hook_event_name": "PostToolUse", "tool_use_id": call, "tool_response": body}) return event, history, payload @@ -99,3 +103,10 @@ def test_incomplete_native_read_cannot_gain_resume_continuity(tmp_path, monkeypa context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) _, wire = project(tmp_path, "second", payload=payload) assert wire["mode"] == "full" and wire["context"] == payload + + +def test_complete_eof_cannot_hide_a_prior_page_missing_from_native_replay(tmp_path, monkeypatch): + event, _, payload = observed_read(tmp_path, monkeypatch, lost_prefix=True) + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == "full" and wire["context"] == payload From a473d6f896544a2b9fa61fc51fbd8ec05ce688e3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:09:22 +0800 Subject: [PATCH 31/40] fix(context): qualify the observed native replay version Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 7 ++--- .../turn_driver/codex_context_history.py | 3 ++- .../test_codex_context_resume.py | 26 ++++++++++++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 5f6eac2fdb..feec7a38cd 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -265,7 +265,7 @@ rollout must retain its digest-bound prefix and every observed reader output, including complete byte coverage of the pinned capture, with matching session, workspace, model and permission mode, and no intervening compaction, rollback, replacement or unknown replay record. This bounded IO -profile targets Codex 0.162.1 paginated rollouts; missing, changed, +profile covers Codex 0.162.1 and 0.163.0-alpha.11.1 paginated rollouts; missing, changed, oversized (over 64 MiB) or unqualified sources fall back to full. It retains the normal-read engineering assumption rather than certifying future model input; it neither scans model requests nor asks the model for an ACK. @@ -425,8 +425,9 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 共享 Codex adapter 可核对完整读取时的原生重放检查点:原生实际选择的物理历史文件, 须保留摘要固定的前缀及每份精确 reader 输出,完整覆盖固定捕获的全部字节; 会话、工作区、模型和权限模式相同, -中间没有压缩、回滚、替换或未知重放记录。此有界 IO profile 面向 Codex 0.162.1 的分页 -rollout;来源缺失、变化、超过 64 MiB 或无法资格化时回退全量。仍采用正常读取后的 +中间没有压缩、回滚、替换或未知重放记录。此有界 IO profile 覆盖 Codex 0.162.1 和 +0.163.0-alpha.11.1 的分页 rollout;来源缺失、变化、超过 64 MiB 或无法资格化时回退全量。 +仍采用正常读取后的 工程假设,不声称证明未来模型输入;不扫描模型请求,也不要求模型 ACK。 开启后的 benchmark 原生子进程自动以仅所有者可访问的权限创建文件,父进程与关闭态 保持原权限策略;既有可组写的重放来源不能建立跨进程连续性。 diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py index 834a3a2f69..1d6a2ed653 100644 --- a/loopx/control_plane/turn_driver/codex_context_history.py +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -54,7 +54,8 @@ def _snapshot(event: dict[str, Any]) -> tuple[dict[str, Any], bytes] | None: meta = first.get("payload", {}) if (first.get("type") != "session_meta" or meta.get("id") != event.get("session_id") or meta.get("cwd") != event.get("cwd") or meta.get("forked_from_id") - or meta.get("history_mode") != "paginated" or meta.get("cli_version") != "0.162.1" + or meta.get("history_mode") != "paginated" + or meta.get("cli_version") not in ("0.162.1", "0.163.0-alpha.11.1") or not event.get("model") or not event.get("permission_mode")): return None # Filesystem identities are opaque; large inode numbers must survive the diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index c2d6200aad..acf6995fad 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -13,11 +13,12 @@ def append(path, kind, payload): stream.write(json.dumps({"type": kind, "payload": payload}) + "\n") -def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost_prefix=False): +def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost_prefix=False, + version="0.162.1"): event = setup(root, monkeypatch) history = root / "rollout.jsonl" append(history, "session_meta", {"id": "session", "cwd": str(root), - "cli_version": "0.162.1", "history_mode": "paginated"}) + "cli_version": version, "history_mode": "paginated"}) history.chmod(0o600) event.update(transcript_path=str(history), model="fixture", permission_mode="bypassPermissions") append(history, "turn_context", {"cwd": str(root), "model": "fixture"}) @@ -41,8 +42,9 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost @pytest.mark.parametrize("shell_wrapper", [False, True]) -def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_path, monkeypatch, shell_wrapper): - event, history, payload = observed_read(tmp_path, monkeypatch, shell_wrapper=shell_wrapper) +@pytest.mark.parametrize("version", ["0.162.1", "0.163.0-alpha.11.1"]) +def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_path, monkeypatch, shell_wrapper, version): + event, history, payload = observed_read(tmp_path, monkeypatch, shell_wrapper=shell_wrapper, version=version) before = context._state(tmp_path) assert before["candidate"]["native_resume"]["inode"].isdigit() append(history, "event_msg", {"type": "task_complete"}) @@ -58,8 +60,9 @@ def test_completed_native_resume_keeps_delta_without_old_capture_prefetch(tmp_pa @pytest.mark.parametrize("change", ["compact", "rollback", "retained", "unknown", "truncate", "replace", "rewrite", "missing", "output", "duplicate", "model", "workspace", "session", "permission", "writable", "malformed"]) -def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, monkeypatch, change): - event, history, payload = observed_read(tmp_path, monkeypatch) +@pytest.mark.parametrize("version", ["0.162.1", "0.163.0-alpha.11.1"]) +def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, monkeypatch, change, version): + event, history, payload = observed_read(tmp_path, monkeypatch, version=version) before = context._state(tmp_path) if change in {"compact", "retained", "unknown"}: append(history, {"compact": "compacted", "retained": "retained_context", "unknown": "future_history"}[change], {}) @@ -98,6 +101,17 @@ def test_resume_loss_or_unknown_continuity_falls_back_to_current_full(tmp_path, assert wire["mode"] == "full" and wire["context"] == payload +@pytest.mark.parametrize("version", ["0.163.0", "0.163.0-alpha.11.2", "unknown"]) +def test_unqualified_native_version_cannot_retain_resume_baseline(tmp_path, monkeypatch, version): + event, _, payload = observed_read(tmp_path, monkeypatch, version=version) + before = context._state(tmp_path) + assert "native_resume" not in before["candidate"] + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + assert context._state(tmp_path)["context_epoch"] != before["context_epoch"] + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == "full" and wire["context"] == payload + + def test_incomplete_native_read_cannot_gain_resume_continuity(tmp_path, monkeypatch): event, _, payload = observed_read(tmp_path, monkeypatch, complete=False) context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) From 9ec9f7550f050479a85678cec8eb95be30c55184 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Mon, 26 Oct 2026 19:29:56 +0800 Subject: [PATCH 32/40] fix(context): qualify native SSH delivery with scoped replay evidence Keep the shared lifecycle owner and verify complete reader byte ranges. Scope native resume checks to the active baseline reader interval so unrelated physical history size does not disable reuse. Add SSH Goal configuration and native recovery coverage while preserving feature-off behavior. Signed-off-by: huangruiteng --- docs/reference/required-work-context.md | 39 ++- .../machine_configuration/context_delivery.py | 2 +- loopx/control_plane/heartbeat/host.py | 2 +- loopx/control_plane/heartbeat/task_body.py | 11 +- .../turn_driver/codex_context.py | 9 +- .../turn_driver/codex_context_history.py | 225 +++++++++++++----- .../test_codex_context_capture.py | 75 ++++-- .../test_codex_context_lifecycle.py | 5 +- .../test_codex_context_resume.py | 56 ++++- .../test_context_machine_configuration.py | 8 +- .../test_heartbeat_envelope_transport.py | 39 +++ 11 files changed, 364 insertions(+), 107 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index feec7a38cd..05dc3c6beb 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -200,8 +200,8 @@ machine default, with off when absent. The existing worker environment's explici not enable delivery. Benchmark trials keep their existing `context_delivery=on` opt-in. Workers propagate the resolved trial choice, including default off, to both preflight and solver subprocesses without changing the caller's settings. -On accepts only the two existing declared scenarios: Codex App heartbeat (`--codex-app` or its equivalent complete scheduler -declaration), and the existing Codex benchmark heartbeat worker (`generic_cli` +On accepts the declared native scenarios: Codex App heartbeat (`--codex-app` or its equivalent complete scheduler +declaration), Codex App SSH Goal (`--runtime-profile codex_app_ssh_goal`), and the existing Codex benchmark heartbeat worker (`generic_cli` plus its explicit heartbeat worker environment). Missing, contradictory or unsupported Codex declarations return an error before admission. This machine preference is scoped to Codex: other explicitly resolved hosts keep full delivery. @@ -246,7 +246,10 @@ 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. It does not install or trust hooks, +existing hooks and recovery instructions. 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, change host configuration or restart the running App. Configure and trust its exact hooks for the next native session. The native path preserves authentication, endpoints, provider selection and connection mode, including implicit history via @@ -261,12 +264,19 @@ it. A resume may supply different history under the same session ID; the [native event contract](https://learn.chatgpt.com/docs/hooks#sessionstart) alone does not attest retention. The shared Codex adapter can qualify a resume against the complete read's native replay checkpoint: the native-selected physical -rollout must retain its digest-bound prefix and every observed reader output, +rollout must retain the digest-bound interval beginning at the current baseline's +first reader and every observed reader output, including complete byte coverage of the pinned capture, with matching session, workspace, model and permission mode, and no intervening compaction, rollback, replacement or unknown replay record. This bounded IO -profile covers Codex 0.162.1 and 0.163.0-alpha.11.1 paginated rollouts; missing, changed, -oversized (over 64 MiB) or unqualified sources fall back to full. It retains the +profile is qualified on Codex 0.162.1 paginated rollouts. It streams only the +baseline's reader interval and subsequent records; unrelated earlier history is +neither hashed nor decoded, and total file size does not decide reuse. The +session metadata's creator version is not the running host version. Code-mode +parent emissions must retain the same complete page bytes observed by nested +reader hooks. Missing or changed evidence and unqualified replay records fall +back to full. One decoded record remains bounded; this is not a history-size +limit. Legacy whole-prefix checkpoints require a fresh complete read. It retains the normal-read engineering assumption rather than certifying future model input; it neither scans model requests nor asks the model for an ACK. Opted-in benchmark native children create owner-only files automatically; the @@ -383,8 +393,8 @@ TurnEnvelope。在基线连续性成立的口径内,两入口仍逐轮发送 ` 已有 worker 环境的 `LOOPX_CONTEXT_DELIVERY=off` 也覆盖继承;环境 on 本身不启用投递。 benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本次运行已解析的选择 (含默认关闭)传给预检和 solver 子进程,不改写调用方原设置。 -开启仅接受已有两类声明:Codex App 心跳(`--codex-app` 或等价完整调度声明),以及 -现有 Codex benchmark 心跳 worker(`generic_cli` 与其显式心跳环境)。缺失、冲突、 +开启接受以下原生声明:Codex App 心跳(`--codex-app` 或等价完整调度声明)、 +Codex App SSH Goal(`--runtime-profile codex_app_ssh_goal`),以及现有 Codex benchmark 心跳 worker(`generic_cli` 与其显式心跳环境)。缺失、冲突、 不支持的 Codex 声明在准入前报错。机器偏好仅作用于 Codex;其他已明确解析的宿主 继续完整投递,对这些宿主显式指定 `--context-delivery on` 仍报错。声明只选择 adapter, 不证明宿主身份或上下文保留。 @@ -398,6 +408,8 @@ benchmark trial 保留既有 `context_delivery=on` 启用方式。worker 将本 机器开启后,`quota context-config --codex-app --goal-id GOAL --agent-id AGENT` 只准备 私有传输末级目录并生成已有 hook 和恢复指令,不安装或信任 hook,也不更改宿主配置。 +SSH Goal 使用 `--runtime-profile codex_app_ssh_goal` 生成 context-config 和 thin/bootstrap 提示, +沿用可见 Goal 的执行与结算身份;仅启用差分时接入捕获目录,不转换成自动心跳。 原生 `quota should-run` 与已保存捕获的 `context-project` 共用分页呈现,直接返回第零页、 固定摘要的后续读取与完整恢复,不另行准入。内联页保持 3 KiB;后续命令最多请求 8 KiB 正文,并用 `--max-page-output-bytes 7900` 将成功 JSON stdout(含转义、 @@ -423,11 +435,14 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 `PostCompact`、清空、分叉、会话/工作区切换使基线失效;同一会话 ID 的 resume 也可能使用不同历史,[原生事件契约](https://learn.chatgpt.com/docs/hooks#sessionstart)本身不证明保留。 共享 Codex adapter 可核对完整读取时的原生重放检查点:原生实际选择的物理历史文件, -须保留摘要固定的前缀及每份精确 reader 输出,完整覆盖固定捕获的全部字节; +须保留从当前基线首次 reader 开始的摘要固定区间,以及每份精确 reader 输出,完整覆盖固定捕获的全部字节; 会话、工作区、模型和权限模式相同, -中间没有压缩、回滚、替换或未知重放记录。此有界 IO profile 覆盖 Codex 0.162.1 和 -0.163.0-alpha.11.1 的分页 rollout;来源缺失、变化、超过 64 MiB 或无法资格化时回退全量。 -仍采用正常读取后的 +中间没有压缩、回滚、替换或未知重放记录。此 IO profile 在 Codex 0.162.1 的分页 +rollout 上验证;流式读取当前基线对应区间及后续记录,不哈希或解码无关旧历史, +文件总大小不决定是否差分。session metadata 的创建版本不等于当前宿主版本。 +code-mode 父调用的保留输出须包含嵌套 reader hook 观察到的全部精确页字节。 +证据缺失、改写或重放记录不受支持时回退全量;单条记录的解码内存仍有边界, +不是历史文件容量限制。旧版全前缀检查点须重新完整读取后建立新检查点。仍采用正常读取后的 工程假设,不声称证明未来模型输入;不扫描模型请求,也不要求模型 ACK。 开启后的 benchmark 原生子进程自动以仅所有者可访问的权限创建文件,父进程与关闭态 保持原权限策略;既有可组写的重放来源不能建立跨进程连续性。 diff --git a/loopx/capabilities/machine_configuration/context_delivery.py b/loopx/capabilities/machine_configuration/context_delivery.py index b1d41d99ad..349f34745b 100644 --- a/loopx/capabilities/machine_configuration/context_delivery.py +++ b/loopx/capabilities/machine_configuration/context_delivery.py @@ -28,7 +28,7 @@ def context_delivery_machine_configuration_namespace() -> MachineConfigurationNa apply_public_update=lambda _current, update: dict(update), title="Differential context", description=( - "Reuse unchanged context for Codex App and benchmark heartbeats. Off by default; " + "Reuse unchanged context for Codex App heartbeats, SSH Goals and benchmark heartbeats. Off by default; " "unsupported declarations fail and known context loss keeps full context. " "Completed reads use an explicit native-hook continuity assumption. " "A saved preference does not establish host readiness or actual delta use." diff --git a/loopx/control_plane/heartbeat/host.py b/loopx/control_plane/heartbeat/host.py index e7544a736c..78796997bf 100644 --- a/loopx/control_plane/heartbeat/host.py +++ b/loopx/control_plane/heartbeat/host.py @@ -49,7 +49,7 @@ def resolve_heartbeat_capture_root( if turn_envelope: raise ValueError("TurnEnvelope requires an explicit decision capture root") return None - if (not thin or full or compact or brief or native_goal_host + if (not thin or full or compact or brief or (native_goal_host and not enabled) or (not turn_instance_id and codex_context_root is None)): raise ValueError("--decision-output-root requires thin mode and an explicit host-owned Turn ID") directory = Path(root).expanduser().absolute() diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index 15d1181f69..f6252d508a 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -437,6 +437,7 @@ def render_visible_goal_task_body( compact_prompt_command: str, brief_prompt_command: str, thin_prompt_command: str, + captured_envelope: bool = False, ) -> str: del ( cli_preflight, @@ -459,6 +460,7 @@ def render_visible_goal_task_body( permission_rule=permission_rule, agent_scope_instruction=agent_scope_instruction, host_wait_rule=CODEX_NATIVE_GOAL_UNCHANGED_WAIT_RULE, + captured_envelope=captured_envelope, ) def render_traex_visible_goal_task_body( *, @@ -519,6 +521,7 @@ def _render_goal_task_body( agent_scope_instruction: str, host_wait_rule: str, work_rule: str = SCOPE_BOUNDED_WORK_RULE, + captured_envelope: bool = False, ) -> str: scope_block = f"\n{agent_scope_instruction}\n" if agent_scope_instruction else "" prequota_block = ( @@ -530,6 +533,12 @@ def _render_goal_task_body( material_queue_rule=material_queue_rule, permission_rule=permission_rule, ) + envelope_rule = ( + "For TurnEnvelope, honor action/boundary/required_reads; read " + "detail_ref.full_decision for the complete same-invocation contract " + "before selection, work or settlement. Retain transport flags on fresh guards.\n" + if captured_envelope else "" + ) return f"""Advance goal `{goal_id}` from `{active_state}` {host_preamble} {scope_block} @@ -539,7 +548,7 @@ def _render_goal_task_body( {prequota_block}Each work iteration: read complete successful JSON: `{quota_guard_command}` -Use current `interaction_contract`. +{envelope_rule}Use current `interaction_contract`. {HOST_LOOP_QUOTA_DISPATCH_RULE} Preserve command flags; truthful evidence. Repair entrypoints within authority. Unavailable/incomplete contract: no work/spend/completion claim. diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 3887dd8726..d33962d79f 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -24,7 +24,7 @@ # Native adapter activation reuses scheduler-owned host declarations. These # settings are presentation opt-ins, never host authentication or authority. CONTEXT_DELIVERY_SETTINGS = ("off", "on") -CONTEXT_DELIVERY_SCENARIOS = ("codex_benchmark_heartbeat", "codex_app_heartbeat") +CONTEXT_DELIVERY_SCENARIOS = ("codex_benchmark_heartbeat", "codex_app_heartbeat", "codex_app_ssh_goal") CONTEXT_LIFECYCLE_HOOKS = ("SessionStart", "PreCompact", "PostCompact", "PostToolUse", "Interrupt") @@ -81,6 +81,10 @@ def context_delivery_activation(setting: str, *, runtime_profile: str | None = N and context.scheduler_owner is SchedulerOwner.HOST_AUTOMATION and context.execution_mode is ExecutionMode.HOSTED_AUTOMATION): scenario = "codex_app_heartbeat" + elif (context.host_surface is HostSurface.CODEX_APP_SSH + and context.scheduler_owner is SchedulerOwner.AGENT_CLI_LOOP + and context.execution_mode is ExecutionMode.INTERACTIVE): + scenario = "codex_app_ssh_goal" elif (context.host_surface is HostSurface.GENERIC_CLI and context.scheduler_owner is SchedulerOwner.AGENT_CLI_LOOP and context.execution_mode is ExecutionMode.INTERACTIVE @@ -90,6 +94,7 @@ def context_delivery_activation(setting: str, *, runtime_profile: str | None = N raise ValueError( "--context-delivery on requires a supported declared run mode: " "Codex App heartbeat (--codex-app or --runtime-profile codex_app_heartbeat), " + "Codex App SSH Goal (--runtime-profile codex_app_ssh_goal), " "or the Codex benchmark heartbeat worker. Missing, contradictory or unsupported " "declarations are rejected; use --context-delivery off for ordinary full delivery." ) @@ -255,7 +260,7 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, *cli_argv, "--format", "json", "quota", "context-hook", *([] if inherit_machine else ["--context-delivery", "on"]), *(["--codex-app"] if scenario == "codex_app_heartbeat" else - ["--runtime-profile", "generic_cli"]), + ["--runtime-profile", "codex_app_ssh_goal" if scenario == "codex_app_ssh_goal" else "generic_cli"]), option, directory]), "timeout": 10} return {"hooks": {event: [{**({"matcher": "^Bash$"} if event == "PostToolUse" else {}), "hooks": [handler]}] for event in CONTEXT_LIFECYCLE_HOOKS}, diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py index 1d6a2ed653..016b6871f5 100644 --- a/loopx/control_plane/turn_driver/codex_context_history.py +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -2,8 +2,9 @@ SessionStart(resume) is emitted after native history reconstruction. Reuse still assumes the qualified native host replays its selected append-only source. Bind -that source to an observed complete read, require its unchanged physical prefix -and exact persisted last reader output, and reject unknown/lossy replay tails. +that source to the current baseline's reader interval, verify every completed +read and reject unknown/lossy replay tails. Earlier unrelated history is not a +dependency of this baseline and its size is not an eligibility condition. This is not a scan of model inputs or a certificate from a session ID/journal. """ from __future__ import annotations @@ -11,10 +12,13 @@ import hashlib import json import os +from contextlib import contextmanager +from collections import Counter +from collections.abc import Iterator from pathlib import Path import re import stat -from typing import Any +from typing import Any, BinaryIO # A compatibility profile of the native rollout, not a shared control vocabulary. # Unknown record types fall back to full until their replay semantics qualify. @@ -25,7 +29,9 @@ "agent_reasoning_raw_content_delta", "agent_reasoning_delta", "agent_message_delta", "hook_started", "hook_completed", "warning", "error", "stream_error", "turn_aborted", "terminal_interaction", "background_event"} -_MAX_HISTORY_BYTES = 64 * 1024 * 1024 +_IO_CHUNK_BYTES = 64 * 1024 +# Bound one decoded protocol record, not the history or the replay interval. +_MAX_RECORD_BYTES = 16 * 1024 * 1024 _SHELL_OUTPUT = re.compile(r"\AChunk ID: [^\r\n]+\nWall time: [0-9.]+ seconds\n" r"Process exited with code 0\n(?:Original token count: [0-9]+\n)?Output:\n(.*)\Z", re.S) @@ -35,34 +41,98 @@ def _digest(value: object) -> str: separators=(",", ":")).encode()).hexdigest() -def _snapshot(event: dict[str, Any]) -> tuple[dict[str, Any], bytes] | None: +@contextmanager +def _snapshot(event: dict[str, Any]) -> Iterator[tuple[dict[str, Any], BinaryIO]]: path = Path(event.get("transcript_path") or "") if not path.is_absolute() or path.resolve(strict=True) != path: - return None + raise ValueError("native replay source must be physical and absolute") descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) with os.fdopen(descriptor, "rb") as stream: info = os.fstat(stream.fileno()) if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_ino != path.lstat().st_ino - or info.st_mode & 0o022 or not 0 < info.st_size <= _MAX_HISTORY_BYTES): - return None - raw = stream.read(info.st_size + 1) + or info.st_mode & 0o022 or info.st_size <= 0): + raise ValueError("native replay source must be an owned regular file") + first_raw = stream.readline(_MAX_RECORD_BYTES + 1) + if len(first_raw) > _MAX_RECORD_BYTES or not first_raw.endswith(b"\n"): + raise ValueError("invalid native session metadata") + first = json.loads(first_raw) + meta = first.get("payload", {}) + if (first.get("type") != "session_meta" or meta.get("id") != event.get("session_id") + or meta.get("cwd") != event.get("cwd") or meta.get("forked_from_id") + or meta.get("history_mode") != "paginated" + or meta.get("cli_version") not in ("0.153.4", "0.162.1", "0.163.0-alpha.11.1") + or not event.get("model") or not event.get("permission_mode")): + raise ValueError("unqualified native replay metadata") + stream.seek(info.st_size - 1) + if stream.read(1) != b"\n": + raise ValueError("native replay observation ends within a record") + # cli_version describes the original writer, not the current host. + # Qualify the supported replay records below, including after upgrade. + facts = {"path": str(path), "device": str(info.st_dev), "inode": str(info.st_ino), + "metadata_sha256": hashlib.sha256(first_raw).hexdigest(), + "observed_bytes": info.st_size, "model": event["model"], + "permission_mode": event["permission_mode"]} + yield facts, stream after = os.fstat(stream.fileno()) - if len(raw) != info.st_size or after.st_size != info.st_size or not raw.endswith(b"\n"): - return None - first = json.loads(raw.split(b"\n", 1)[0]) - meta = first.get("payload", {}) - if (first.get("type") != "session_meta" or meta.get("id") != event.get("session_id") - or meta.get("cwd") != event.get("cwd") or meta.get("forked_from_id") - or meta.get("history_mode") != "paginated" - or meta.get("cli_version") not in ("0.162.1", "0.163.0-alpha.11.1") - or not event.get("model") or not event.get("permission_mode")): - return None - # Filesystem identities are opaque; large inode numbers must survive the - # shared TypeScript JSON boundary without IEEE-754 integer rounding. - return {"path": str(path), "device": str(info.st_dev), "inode": str(info.st_ino), - "prefix_bytes": len(raw), "prefix_sha256": hashlib.sha256(raw).hexdigest(), - "model": event["model"], "permission_mode": event["permission_mode"]}, raw + if (after.st_size != info.st_size or after.st_mtime_ns != info.st_mtime_ns + or path.lstat().st_ino != info.st_ino): + raise ValueError("native replay source changed during observation") + + +def _records(stream: BinaryIO, start: int, end: int) -> Iterator[tuple[int, dict[str, Any]]]: + stream.seek(start) + while stream.tell() < end: + offset = stream.tell() + raw = stream.readline(min(_MAX_RECORD_BYTES + 1, end - offset)) + if len(raw) > _MAX_RECORD_BYTES or not raw.endswith(b"\n"): + raise ValueError("incomplete or unsupported native replay record") + yield offset, json.loads(raw) + + +def _region_digest(stream: BinaryIO, start: int, end: int) -> str: + digest = hashlib.sha256() + stream.seek(start) + remaining = end - start + while remaining: + chunk = stream.read(min(_IO_CHUNK_BYTES, remaining)) + if not chunk: + raise ValueError("native reader interval was truncated") + digest.update(chunk) + remaining -= len(chunk) + return digest.hexdigest() + + +def _reader_start(stream: BinaryIO, end: int, pages: list[dict[str, Any]]) -> int: + """Locate this reader at the tail, stopping at its native call boundary. + + A nested code-mode hook may run before the parent output is appended. In + that case the call record anchors the following output, which must exist + and pass complete coverage on resume. Never search the unrelated prefix. + """ + cursor, buffer = end, b"" + while cursor: + start = max(0, cursor - _IO_CHUNK_BYTES) + stream.seek(start) + buffer = stream.read(cursor - start) + buffer + cursor = start + while (split := buffer.rstrip(b"\n").rfind(b"\n")) >= 0: + raw, buffer = buffer[split + 1:], buffer[:split + 1] + if len(raw) > _MAX_RECORD_BYTES: + raise ValueError("unsupported native reader record") + record = json.loads(raw) + payload = record.get("payload", {}) + offset = cursor + split + 1 + if record.get("type") == "response_item": + if _read_pages(payload.get("output")) == pages: + return offset + if payload.get("type") in {"function_call", "custom_tool_call"}: + return offset + if record.get("type") == "event_msg" and payload.get("type") == "task_started": + return end + if len(buffer) > _MAX_RECORD_BYTES: + raise ValueError("unsupported native reader record") + return end def _read_pages(output: object) -> list[dict[str, Any]]: @@ -71,19 +141,22 @@ def _read_pages(output: object) -> list[dict[str, Any]]: # This is the native exec_command result envelope, not a prose classifier. if isinstance(output, str) and (match := _SHELL_OUTPUT.fullmatch(output)): output = match[1] + if isinstance(output, list): + return [page for item in output if isinstance(item, dict) and item.get("type") == "input_text" + for page in _read_pages(item.get("text"))] return _context_pages(output) -def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, +def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], stream: BinaryIO, event: dict[str, Any]) -> bool: from .decision_capture import capture_page_range, read_context_capture_file if any(anchor.get(k) != snapshot.get(k) for k in - ("path", "device", "inode", "model", "permission_mode")): + ("path", "device", "inode", "metadata_sha256", "model", "permission_mode")): return False - end = anchor.get("prefix_bytes") - if (type(end) is not int or not 0 < end <= len(raw) or raw[end - 1:end] != b"\n" - or hashlib.sha256(raw[:end]).hexdigest() != anchor.get("prefix_sha256")): + start, end = anchor.get("reader_start"), anchor.get("observed_bytes") + if (type(start) is not int or type(end) is not int or not 0 < start <= end <= snapshot["observed_bytes"] + or _region_digest(stream, start, end) != anchor.get("reader_sha256")): return False reads, reference = anchor.get("reads"), anchor.get("read_source") if not isinstance(reads, list) or not reads or not isinstance(reference, dict): @@ -95,17 +168,24 @@ def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, # Verify every observed read, not just EOF. A complete last page must never # hide an earlier output that the native replay dropped or truncated. seen: set[str] = set() + expected_pages = Counter(required.values()) + seen_pages: Counter[str] = Counter() ranges = [] - for line in raw.splitlines(): - record = json.loads(line) + for _, record in _records(stream, start, snapshot["observed_bytes"]): payload = record.get("payload", {}) - if (record.get("type") == "response_item" and payload.get("type") == "function_call_output" - and payload.get("call_id") in required): + native_output = payload.get("type") in {"function_call_output", "custom_tool_call_output"} + if record.get("type") == "response_item" and native_output: call = payload["call_id"] pages = _read_pages(payload.get("output")) - if call in seen or _digest(pages) != required[call]: - return False - seen.add(call) + digest = _digest(pages) + if call in required: + if call in seen or digest != required[call]: + return False + seen.add(call) + elif payload.get("type") != "custom_tool_call_output" or digest not in expected_pages: + pages = [] + if pages: + seen_pages[digest] += 1 for page in pages: if page.get("sha256") != reference["sha256"]: continue @@ -113,26 +193,29 @@ def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], raw: bytes, if span is None: return False ranges.append(span) + if not _append_only(record, event): + return False cursor = 0 for start, stop in sorted(ranges): if start > cursor: return False cursor = max(cursor, stop) - if seen != set(required) or cursor != len(body): + if seen_pages != expected_pages or cursor != len(body): return False - for line in raw[end:].splitlines(): - record = json.loads(line) - kind, payload = record.get("type"), record.get("payload", {}) - if kind == "event_msg": - if payload.get("type") not in _APPEND_EVENTS: - return False - # Changes to history mode are not ordinary append-only settings. - if payload.get("type") == "thread_settings_applied" and payload.get("history_mode") not in (None, "paginated"): - return False - elif kind not in _APPEND_RECORDS: - return False # includes compaction, retained-history replacement and rollback - if kind == "turn_context" and (payload.get("cwd") != event["cwd"] or payload.get("model") != event["model"]): + return True + + +def _append_only(record: dict[str, Any], event: dict[str, Any]) -> bool: + kind, payload = record.get("type"), record.get("payload", {}) + if kind == "event_msg": + if payload.get("type") not in _APPEND_EVENTS: return False + if payload.get("type") == "thread_settings_applied" and payload.get("history_mode") not in (None, "paginated"): + return False + elif kind not in _APPEND_RECORDS: + return False + if kind == "turn_context" and (payload.get("cwd") != event["cwd"] or payload.get("model") != event["model"]): + return False return True @@ -150,7 +233,15 @@ def observe_native_read(state: dict[str, Any], event: dict[str, Any], reads = [] if read not in reads: reads = [*reads, read] - return {**state, "pending": {**pending, "native_reads": reads}} + pending = {**pending, "native_reads": reads} + if "native_reader_start" not in pending: + try: + with _snapshot(event) as (facts, stream): + start = _reader_start(stream, facts["observed_bytes"], pages) + pending["native_reader_start"] = {**facts, "reader_start": start} + except (OSError, ValueError, TypeError, KeyError, AttributeError, RecursionError): + pass + return {**state, "pending": pending} def native_resume_verified(state: dict[str, Any], event: dict[str, Any]) -> bool: @@ -163,8 +254,9 @@ def native_resume_verified(state: dict[str, Any], event: dict[str, Any]) -> bool or state.get("session_id") != event.get("session_id") or state.get("cwd") != event.get("cwd")): return False try: - snapshot = _snapshot(event) - return snapshot is not None and _matches(anchor, *snapshot, event) + with _snapshot(event) as (facts, stream): + verified = _matches(anchor, facts, stream, event) + return verified except (OSError, ValueError, TypeError, KeyError, AttributeError, RecursionError): return False @@ -181,20 +273,27 @@ def checkpoint_native_read(state: dict[str, Any], updated: dict[str, Any], or candidate == state.get("candidate") or not event.get("tool_use_id")): return try: - snapshot = _snapshot(event) - if snapshot is None: - return - facts, raw = snapshot - if candidate.get("ancestry", 0): - anchor = (state.get("candidate") or {}).get("native_resume") - if not isinstance(anchor, dict) or not _matches(anchor, facts, raw, event): - return pending = state.get("pending") or {} reference = pending if candidate.get("ancestry", 0) else state.get("recovery") reads = pending.get("native_reads") - if not isinstance(reference, dict) or not isinstance(reads, list) or not reads: + start = pending.get("native_reader_start") + if not isinstance(reference, dict) or not isinstance(reads, list) or not reads or not isinstance(start, dict): return - candidate["native_resume"] = {**facts, "reads": reads, - "read_source": {key: reference[key] for key in ("path", "sha256")}} + with _snapshot(event) as (facts, stream): + if any(start.get(k) != facts.get(k) for k in ("path", "device", "inode", "metadata_sha256")): + return + reader_start = start["reader_start"] + if candidate.get("ancestry", 0): + anchor = (state.get("candidate") or {}).get("native_resume") + if not isinstance(anchor, dict) or not _matches(anchor, facts, stream, event): + return + # A delta still depends on its earlier complete baseline. + reader_start = min(reader_start, anchor["reader_start"]) + if not 0 < reader_start <= facts["observed_bytes"]: + return + checkpoint = {**facts, "reader_start": reader_start, + "reader_sha256": _region_digest(stream, reader_start, facts["observed_bytes"]), + "reads": reads, "read_source": {key: reference[key] for key in ("path", "sha256")}} + candidate["native_resume"] = checkpoint except (OSError, ValueError, TypeError, KeyError, AttributeError, RecursionError): return diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index a70721af94..0f03790925 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -87,7 +87,10 @@ def _last_page(request): + [(host, surface, False, True, False) for host in ("benchmark", "code_mode") for surface in ("quota_packet", "turn_envelope")] + [("code_mode", surface, False, True, True) for surface in ("quota_packet", "turn_envelope")]) -def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False): +def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False, ssh_goal=False): + # Real worker qualification follows returned cursors; output budgets can + # shrink a page, so precomputed fixed offsets are not its production path. + bounded_pages = bounded_pages or host == "benchmark" isolate_sqlite_runtime(tmp_path, monkeypatch) binary, env, workspace, home = _native_fixture(tmp_path) env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) @@ -114,16 +117,17 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho installed_cli = os.environ.get("LOOPX_NATIVE_CONTEXT_CLI") if installed_cli: cli = [installed_cli, "--cli-bin", installed_cli] + host_args = ["--runtime-profile", "codex_app_ssh_goal"] if ssh_goal else ["--codex-app"] context_option = "--codex-context-root" if shared_root else "--codex-context-dir" native_configuration = None if shared_root: prepared = subprocess.run(cli + ["--format", "json", "quota", "context-config", - "--context-delivery", "on", "--codex-app", context_option, str(lifecycle), "--goal-id", GOAL_ID, "--agent-id", AGENT_ID], + "--context-delivery", "on", *host_args, context_option, str(lifecycle), "--goal-id", GOAL_ID, "--agent-id", AGENT_ID], capture_output=True, text=True, timeout=15) assert prepared.returncode == 0, prepared.stderr native_configuration = json.loads(prepared.stdout)["native_config"] hook_command = (native_configuration["hooks"]["SessionStart"][0]["hooks"][0]["command"] - if native_configuration else shlex.join(cli + ["quota", "context-hook", "--context-delivery", "on", "--codex-app", context_option, str(lifecycle)])) + if native_configuration else shlex.join(cli + ["quota", "context-hook", "--context-delivery", "on", *host_args, context_option, str(lifecycle)])) if failed_primary: # Fault only the primary journal update. Production code must still # return the original full-read reference; no test recovery handler. @@ -151,21 +155,26 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho f"os.environ['PYTHONPATH']={str(Path(__file__).resolve().parents[2])!r}\n" + f"os.execv({sys.executable!r}, [{sys.executable!r}, '-m', 'loopx.entrypoint', *sys.argv[1:]])\n")) wrapper.chmod(0o700) - hook_command = shlex.join([str(wrapper), "quota", "context-hook", "--context-delivery", "on", "--codex-app", context_option, str(lifecycle)]) + hook_command = shlex.join([str(wrapper), "quota", "context-hook", "--context-delivery", "on", *host_args, context_option, str(lifecycle)]) handler = {"type": "command", "command": hook_command, "timeout": 10} if host != "benchmark": from loopx.control_plane.turn_driver.codex_context import CONTEXT_LIFECYCLE_HOOKS - (home / "hooks.json").write_text(json.dumps({"hooks": { + hooks = (json.loads(json.dumps(native_configuration["hooks"])) if native_configuration else { event: [{**({"matcher": "^Bash$"} if event == "PostToolUse" else {}), - "hooks": [handler]}] for event in CONTEXT_LIFECYCLE_HOOKS}})) + "hooks": [dict(handler)]}] for event in CONTEXT_LIFECYCLE_HOOKS}) + for groups in hooks.values(): + for group in groups: + for configured in group["hooks"]: + configured["command"] = hook_command + (home / "hooks.json").write_text(json.dumps({"hooks": hooks})) errors, snapshots = [], [] stages = {"phase": "start", "pages": [], "baseline_confirmed": False, "compact_response": None} wires, page_digests, read_paths = {}, {}, {} command = [*cli, "--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", "quota", "should-run", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, - "--todo-id", TODO_ID, "--turn-instance-id", "native-capture-turn", "--codex-app", - "--decision-output-root", str(root), "--context-delivery", "on", "--codex-app", context_option, str(lifecycle)] + "--todo-id", TODO_ID, "--turn-instance-id", "native-capture-turn", + "--decision-output-root", str(root), "--context-delivery", "on", *host_args, context_option, str(lifecycle)] if handoff_socket: command += ["--decision-handoff-socket", handoff_socket] if surface == "turn_envelope": @@ -173,16 +182,22 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho generated_guard = None if shared_root: generated = subprocess.run([*cli, "--registry", str(registry), "--runtime-root", str(runtime), - "--format", "json", "heartbeat-prompt", "--thin", "--codex-app", + "--format", "json", "heartbeat-prompt", "--thin", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--decision-output-root", str(root), - "--context-delivery", "on", "--codex-app", context_option, str(lifecycle), *(["--turn-envelope"] if surface == "turn_envelope" else [])], + "--context-delivery", "on", *host_args, context_option, str(lifecycle), *(["--turn-envelope"] if surface == "turn_envelope" else [])], capture_output=True, text=True, timeout=15) - assert generated.returncode == 0, generated.stderr + assert generated.returncode == 0, generated.stderr or generated.stdout prompt = json.loads(generated.stdout) assert prompt["ok"], prompt - generated_guard = next(line for line in prompt["task_body"].splitlines() if "quota should-run" in line) - assert '${LOOPX_TURN:?}' in generated_guard and context_option in generated_guard - generated_guard += " --todo-id " + shlex.quote(TODO_ID) + generated_guard = next(line for line in prompt["task_body"].splitlines() if "quota should-run" in line).strip("`") + assert context_option in generated_guard + if ssh_goal: + assert "--runtime-profile codex_app_ssh_goal" in generated_guard + assert "--heartbeat-turn-receipt" not in generated_guard + else: + assert '${LOOPX_TURN:?}' in generated_guard + if not ssh_goal: + generated_guard += " --todo-id " + shlex.quote(TODO_ID) env["LOOPX_TURN"] = "native-capture-turn" checkpoint_wakes, held_captures = [], [] @@ -192,7 +207,17 @@ def invoke(name): argv = list(command) if checkpoint_handoff: assert argv[argv.index("--decision-handoff-socket") + 1] == checkpoint_wakes[-1].socket_path - return (generated_guard or shlex.join(argv)) + " > " + shlex.quote(str(wire_paths[name])) + " && printf WIRE_READY" + rendered = generated_guard or shlex.join(argv) + if ssh_goal and name == "warm": + # Continue the captured native Turn; a new begin-turn before + # settlement must correctly enter prior-Turn recovery instead. + original = json.loads(snapshots[0][1]) + continued = shlex.split(rendered) + continued.remove("--begin-turn") + 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])) + " && printf WIRE_READY" def pages(name, path): raw = path.read_bytes() @@ -271,7 +296,7 @@ def response(request, number, message): if phase == "resuming": if not resume_loss: assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") - if host == "benchmark": + if host == "benchmark" or ssh_goal: # Actual worker process exit/resume, qualified by the shared # native-selected replay checkpoint. No old full prefetch. assert ("LoopX context baseline invalidated." in _input_text(request)) == bool(resume_loss) @@ -380,7 +405,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, "max_output_tokens": 2000 if bounded_pages else 2400}) + "cmd": command, "login": False, "max_output_tokens": 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} @@ -534,9 +559,12 @@ def held(*args, **kwargs): else: # This large semantic fixture reads full, delta and recovery pages. # Allow their native hook/CLI IO; this is not a latency qualification. - session_id = _app_server(binary, env, workspace, home, True, sandbox="workspace-write", turn_timeout_sec=60) + # The SSH operator is unrestricted. A per-command network sandbox + # cannot share the host-local Effect service across native resumes. + sandbox = "danger-full-access" if ssh_goal else "workspace-write" + session_id = _app_server(binary, env, workspace, home, True, sandbox=sandbox, turn_timeout_sec=60) if resumed: - assert _app_server(binary, env, workspace, home, True, sandbox="workspace-write", + assert _app_server(binary, env, workspace, home, True, sandbox=sandbox, resume=session_id, turn_timeout_sec=60) == session_id assert not errors, errors @@ -574,7 +602,7 @@ def held(*args, **kwargs): current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, capture_id=wires["warm"]["target"]["capture_id"], surface=surface) baseline = wires["cold"] - if resumed and host != "benchmark": + if resumed and host != "benchmark" and not ssh_goal: # This path independently reread every original byte after a reset. # Bind those verified bytes to the new native epoch, rather than letting # automatic full fallback conceal an obsolete pre-loss baseline. @@ -684,3 +712,10 @@ def test_native_benchmark_context_keeps_checkpoint_handoff(tmp_path, monkeypatch def test_native_capture_release_preserves_delta_and_recovery(tmp_path, monkeypatch, private_handoff, surface): # noqa: F811 -- fixture injection test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, "code_mode", surface, False, True, True, handoff_socket=private_handoff) + + +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_ssh_goal_keeps_identity_delta_and_full_recovery(tmp_path, monkeypatch, surface): + test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, + "code_mode", surface, False, True, True, "missing", shared_root=True, + native_read_batch=1, bounded_pages=True, ssh_goal=True) diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index 3936c84ff2..e6ee3aadad 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -797,6 +797,9 @@ def test_lost_hook_reference_is_scoped_read_only_and_pins_later_pages(tmp_path, ([], None), (["--codex-app"], "codex_app_heartbeat"), (["--runtime-profile", "codex_app_heartbeat"], "codex_app_heartbeat"), + (["--runtime-profile", "codex_app_ssh_goal"], "codex_app_ssh_goal"), + (["--host-surface", "codex_app_ssh", "--scheduler-owner", "agent_cli_loop", "--execution-mode", "interactive"], "codex_app_ssh_goal"), + (["--host-surface", "codex_app_ssh", "--scheduler-owner", "host_automation", "--execution-mode", "hosted_automation"], None), (["--host-surface", "codex_app", "--scheduler-owner", "host_automation", "--execution-mode", "hosted_automation"], "codex_app_heartbeat"), (["--host-surface", "codex_cli", "--scheduler-owner", "outer_controller", "--execution-mode", "isolated_headless"], None), (["--runtime-profile", "generic_cli"], None), @@ -835,7 +838,7 @@ def test_activation_reuses_declared_host_mode_and_off_is_inert(tmp_path, monkeyp expected = expected if setting == "on" else None assert activation["requested"] == (setting or "off") assert activation["enabled"] is (expected is not None) and activation["scenario"] == expected - assert activation["supported_scenarios"] == ["codex_benchmark_heartbeat", "codex_app_heartbeat"] + assert activation["supported_scenarios"] == ["codex_benchmark_heartbeat", "codex_app_heartbeat", "codex_app_ssh_goal"] if expected is None: assert "native_config" not in result else: diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index acf6995fad..6f2d6c2c6e 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -14,11 +14,17 @@ def append(path, kind, payload): def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost_prefix=False, - version="0.162.1"): + history_prefix_mib=0, version="0.162.1", code_mode=False): event = setup(root, monkeypatch) history = root / "rollout.jsonl" append(history, "session_meta", {"id": "session", "cwd": str(root), "cli_version": version, "history_mode": "paginated"}) + if history_prefix_mib: + with history.open("ab") as stream: + stream.write(b'{"type":"event_msg","payload":{"type":"background_event","message":"') + for _ in range(history_prefix_mib): + stream.write(b'x' * (1024 * 1024)) + stream.write(b'"}}\n') history.chmod(0o600) event.update(transcript_path=str(history), model="fixture", permission_mode="bypassPermissions") append(history, "turn_context", {"cwd": str(root), "model": "fixture"}) @@ -31,7 +37,13 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost body = json.dumps(page) output = ("Chunk ID: fixture\nWall time: 0.01 seconds\nProcess exited with code 0\n" "Original token count: 1000\nOutput:\n" + body) if shell_wrapper else body - append(history, "response_item", {"type": "function_call_output", "call_id": call, "output": output}) + if code_mode: + # Native code-mode retains the parent emission; the nested hook + # has a different call id. Its actual output must still cover all bytes. + append(history, "response_item", {"type": "custom_tool_call_output", "call_id": f"parent-{i}", + "output": [{"type": "input_text", "text": output}]}) + else: + append(history, "response_item", {"type": "function_call_output", "call_id": call, "output": output}) if lost_prefix and i == len(pages) - 1: rows = [json.loads(line) for line in history.read_text().splitlines()] next(row for row in rows if row["type"] == "response_item")["payload"]["output"] = "truncated" @@ -124,3 +136,43 @@ def test_complete_eof_cannot_hide_a_prior_page_missing_from_native_replay(tmp_pa context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) _, wire = project(tmp_path, "second", payload=payload) assert wire["mode"] == "full" and wire["context"] == payload + + +@pytest.mark.parametrize("prefix_mib", [0, 96]) +def test_unrelated_history_size_does_not_change_replay_or_decode_cost(tmp_path, monkeypatch, prefix_mib): + import tracemalloc + from loopx.control_plane.turn_driver import codex_context_history as history_io + + event, history, payload = observed_read(tmp_path, monkeypatch, history_prefix_mib=prefix_mib) + before = context._state(tmp_path) + assert before["candidate"]["native_resume"]["reader_start"] > prefix_mib * 1024 * 1024 + append(history, "event_msg", {"type": "task_complete"}) + tracemalloc.start() + try: + assert history_io.native_resume_verified(before, {**event, "source": "resume"}) + _, peak = tracemalloc.get_traced_memory() + finally: + tracemalloc.stop() + assert peak < 4 * 1024 * 1024 # no history-sized allocation, including above the former 64 MiB cutoff + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == "delta" + + +def test_old_version_can_resume_current_verified_reader_format(tmp_path, monkeypatch): + event, _, payload = observed_read(tmp_path, monkeypatch, version="0.153.4") + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == "delta" + + +@pytest.mark.parametrize("lose_page", [False, True]) +def test_code_mode_retained_emissions_need_complete_native_read_coverage(tmp_path, monkeypatch, lose_page): + event, history, payload = observed_read(tmp_path, monkeypatch, code_mode=True) + if lose_page: + rows = [json.loads(line) for line in history.read_text().splitlines()] + next(row for row in rows if row["type"] == "response_item")["payload"]["output"] = [] + history.write_text("".join(json.dumps(row) + "\n" for row in rows)) + context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) + _, wire = project(tmp_path, "second", payload=payload) + assert wire["mode"] == ("full" if lose_page else "delta") diff --git a/tests/control_plane/test_context_machine_configuration.py b/tests/control_plane/test_context_machine_configuration.py index 4a941accab..19d30d95ae 100644 --- a/tests/control_plane/test_context_machine_configuration.py +++ b/tests/control_plane/test_context_machine_configuration.py @@ -155,19 +155,19 @@ def test_inherited_on_rejects_unknown_or_unsupported_before_admission(tmp_path, assert _spend_run_count(runtime) == 0 -@pytest.mark.parametrize("profile", ["codex_app", "benchmark"]) +@pytest.mark.parametrize("profile", ["codex_app", "benchmark", "codex_app_ssh_goal"]) @pytest.mark.skipif(os.name != "posix", reason="Native continuity uses POSIX private directories and locks") -def test_native_configuration_inherits_only_the_two_declared_scenarios_and_later_off(tmp_path, monkeypatch, profile): +def test_native_configuration_inherits_declared_scenarios_and_later_off(tmp_path, monkeypatch, profile): registry, runtime = tmp_path / "absent.json", tmp_path / "runtime" assert configure(registry, runtime, tmp_path, "on")[0] == 0 if profile == "benchmark": monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "on") monkeypatch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") - host = ["--codex-app"] if profile == "codex_app" else ["--runtime-profile", "generic_cli"] + host = ["--codex-app"] if profile == "codex_app" else ["--runtime-profile", "codex_app_ssh_goal" if profile == "codex_app_ssh_goal" else "generic_cli"] code, result = _run_cli(registry, runtime, "quota", "context-config", *host, "--goal-id", "fixture", "--agent-id", "worker") assert code == 0 and result["context_delivery"]["enabled"], result - expected = "codex_app_heartbeat" if profile == "codex_app" else "codex_benchmark_heartbeat" + expected = "codex_app_heartbeat" if profile == "codex_app" else ("codex_app_ssh_goal" if profile == "codex_app_ssh_goal" else "codex_benchmark_heartbeat") assert result["context_delivery"]["scenario"] == expected root = runtime / "context-delivery" assert root.stat().st_mode & 0o077 == 0 diff --git a/tests/control_plane/test_heartbeat_envelope_transport.py b/tests/control_plane/test_heartbeat_envelope_transport.py index 9e742e447d..89a1ce4136 100644 --- a/tests/control_plane/test_heartbeat_envelope_transport.py +++ b/tests/control_plane/test_heartbeat_envelope_transport.py @@ -1,4 +1,5 @@ """Qualify the real worker -> renderer -> guard/selection/settlement boundary.""" +import hashlib import json import os import shlex @@ -243,6 +244,7 @@ def test_real_worker_generated_guard_selection_reentry_and_settle_once( if context_delivery: # No native proof in this CLI fixture: composed capture must still # release ordinary full output and preserve reentry and settlement. + (tmp_path / 'context').mkdir(mode=0o700) env.update(LOOPX_CONTEXT_DELIVERY='on', LOOPX_EXECUTION_MODE='heartbeat', LOOPX_CODEX_CONTEXT_DIR=str(tmp_path / 'context')) # Generated CLI reentry uses this process's environment; preserve the # same worker declarations carried by the native tool environment. @@ -257,6 +259,21 @@ def test_real_worker_generated_guard_selection_reentry_and_settle_once( result = subprocess.run(argv, env=env, cwd=project, capture_output=True, text=True) first = json.loads(result.stdout) assert result.returncode == 0, first + def complete_page(value): + if value.get('schema_version') != 'context_capture_page_v1': + return value + digest, body = value['sha256'], b'' + while True: + assert value['ok'] and value['sha256'] == digest and value['offset'] == len(body) + body += value['content'].encode() + if value['eof']: + break + read = subprocess.run(shlex.split(value['next_command']), env=env, cwd=project, + capture_output=True, text=True, check=True) + value = json.loads(read.stdout) + assert hashlib.sha256(body).hexdigest() == digest + return json.loads(body) + first = complete_page(first) root = logs / TURN_ID / 'decisions' captured = enabled or private_handoff is not None or context_delivery if captured: @@ -285,6 +302,7 @@ def test_real_worker_generated_guard_selection_reentry_and_settle_once( '{todo_id}', ALTERNATIVE_TODO_ID) rc, selected = _run_generated_cli(command, registry_path=registry) assert rc == 0, selected + selected = complete_page(selected) if captured: paths = list(root.glob('*/decision.json')) assert len(paths) == 2 @@ -401,3 +419,24 @@ def test_native_context_prompt_keeps_capture_on_reentry(tmp_path, monkeypatch): assert argv[argv.index("--decision-output-root") + 1] == str(tmp_path) assert argv[argv.index("--decision-handoff-socket") + 1] == endpoint assert argv[argv.index("--codex-context-dir") + 1] == context + + +@pytest.mark.parametrize("turn_envelope", [False, True]) +def test_ssh_goal_capture_keeps_native_begin_turn_and_off_boundary(tmp_path, turn_envelope): + args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile="codex_app_ssh_goal", thin=True) + original = build_heartbeat_prompt(**args) + assert original == build_heartbeat_prompt(**args, context_delivery="off") + assert "--decision-output-root" not in original["quota_guard_command"] + with pytest.raises(ValueError, match="host-owned"): + build_heartbeat_prompt(**args, decision_output_root=tmp_path) + enabled = build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=tmp_path, + codex_context_root=str(tmp_path), turn_envelope=turn_envelope) + guard = enabled["quota_guard_command"] + assert "--runtime-profile codex_app_ssh_goal --begin-turn" in guard + assert "--heartbeat-turn-receipt" not in guard and "${LOOPX_TURN" not in guard + assert "--decision-output-root" in guard and "--codex-context-root" in guard + assert "visible-goal" in enabled["quota_spend_command"] + assert enabled["interface_budget"]["within_budget"] + if turn_envelope: + assert "--turn-envelope" in guard + assert "detail_ref.full_decision" in enabled["task_body"] From 6971757767dc1bc88643705e1f4711ed617e2663 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Mon, 26 Oct 2026 19:53:58 +0800 Subject: [PATCH 33/40] fix(context): keep historical monitor receipts out of Goal narrative Recognize only complete identity-bound receipt lines through the existing durable codec. Preserve original recovery bytes and source lineage, along with all authored, malformed, foreign and unknown content. Cover ordinary guard delivery and strict lineage validation. Signed-off-by: huangruiteng --- docs/reference/required-work-context.md | 16 +++++ .../scheduler/monitor_batch_receipts.py | 40 ++++++++++++ .../work_items/context_readback.py | 27 +++++--- .../work_items/interaction_contract.ts | 17 ++++- .../test_goal_narrative_context.py | 65 +++++++++++++++++++ .../interaction_contract.test.ts | 9 +++ 6 files changed, 163 insertions(+), 11 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 05dc3c6beb..b4cb4125da 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -25,6 +25,16 @@ This default read changes the Goal obligation from the mixed document to its complete non-generated text; it does not summarize requirements or erase history. No source file or canonical record is rewritten. +Complete historical Monitor receipt lines are also generated data. The receipt +codec recognizes their exact marker, schema, Goal/operation identity and encoded +wrapper; `excluded_monitor_receipts` retains each line interval and SHA-256. +Their encoded bodies remain in the original file for operation replay, while +the current canonical Todo reads still supply active work and obligations. +Ordinary comments, fenced examples, malformed/unknown receipts, foreign-Goal +receipts and lines with additional prose remain verbatim. This avoids making +an initial full context read carry an accumulated recovery journal. It does not +change receipt replay validation or declare a model-input/token saving. + Unmarked or mixed ownership keeps the original full progressive read in `agent_channel.required_reads`. Malformed markers, non-generated content inside a marked region, unavailable files and invalid UTF-8 fail closed. Unknown @@ -326,6 +336,12 @@ scoped User 读取保留。默认 Goal 必读对象改为完整非生成正文 不重写原文件或权威记录。未标记或混合归属仍须渐进式读取全文;错误标记、标记内非生成 内容、来源缺失及非法 UTF-8 均拒绝。不能按标题猜测或为预算截断正文;依赖交付前须读取 全部已提供上下文与剩余必读,来源改变后重新准入,局部验收不能替代整体目标。 +完整的历史 Monitor 回执行也属于生成数据。回执 codec 核对精确标记、schema、Goal/ +operation 身份及编码外层,`excluded_monitor_receipts` 保留各行的区间和 SHA-256。 +编码正文仍留在原文件供原操作恢复;当前工作和义务仍由 canonical Todo 读取提供。 +普通注释、代码块中的示例、损坏或未知回执、其他 Goal 的回执和附带其他文字的行均原样 +保留。这样首轮全文读取不再携带累积的恢复日志;不改变回执重放校验,也不据此宣称 +模型输入或 token 成本已经降低。 Goal adapter 仍完整读取文件、校验 UTF-8,并以全部字节计算修订摘要;未标记或混合归属 仅在内部桥接传路径与修订,避免未被消费的正文副本,Agent 仍须渐进式全文读取。显式 归属的来源才向 typed owner 传完整非生成正文和区域血缘。字节减少不能证明模型费用、 diff --git a/loopx/control_plane/scheduler/monitor_batch_receipts.py b/loopx/control_plane/scheduler/monitor_batch_receipts.py index 323a04b905..76087cf245 100644 --- a/loopx/control_plane/scheduler/monitor_batch_receipts.py +++ b/loopx/control_plane/scheduler/monitor_batch_receipts.py @@ -36,6 +36,46 @@ def _read_receipt(text: str, operation_id: str) -> dict[str, Any] | None: return receipt +def is_monitor_receipt_line(line: str, *, goal_id: str) -> bool: + """Recognize only the historical writer's complete, identity-bound line. + + This codec check establishes generated ownership, not replay authority. + The original bytes remain available to the typed replay validator. + """ + match = re.fullmatch(r"", + line.rstrip("\r\n")) + if match is None: + return False + + def unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate Monitor receipt field") + result[key] = value + return result + + try: + receipt = json.loads(base64.b64decode(match[2], validate=True), object_pairs_hook=unique_object) + except (ValueError, UnicodeError, RecursionError): + return False + if not isinstance(receipt, dict) or set(receipt) != { + "schema_version", "goal_id", "operation_id", "request_sha256", "writeback"}: + return False + operation = receipt.get("operation_id") + writeback = receipt.get("writeback") + return (receipt.get("schema_version") == "loopx_monitor_batch_receipt_v0" + and receipt.get("goal_id") == goal_id + and isinstance(operation, str) and bool(operation) + and hashlib.sha256(operation.encode()).hexdigest() == match[1] + and isinstance(receipt.get("request_sha256"), str) + and re.fullmatch(r"[0-9a-f]{64}", receipt["request_sha256"]) is not None + and isinstance(writeback, dict) + and writeback.get("schema_version") == "monitor_poll_todo_writeback_v0" + and writeback.get("goal_id") == goal_id + and writeback.get("monitor_effect_id") == operation) + + def _plan(request: dict[str, Any], **facts: Any) -> dict[str, Any]: try: result = effect_runtime_result( diff --git a/loopx/control_plane/work_items/context_readback.py b/loopx/control_plane/work_items/context_readback.py index a185b0d50e..6c8e97fd6a 100644 --- a/loopx/control_plane/work_items/context_readback.py +++ b/loopx/control_plane/work_items/context_readback.py @@ -15,7 +15,8 @@ from ..goals.acceptance import inspect_goal_acceptance from ..goals.state_resolution import resolve_goal_state from ..todos.list_readback import list_goal_todos -from ..todos.machine_region import find_todo_regions +from ..todos.machine_region import find_todo_regions, visible_markdown_lines +from ..scheduler.monitor_batch_receipts import is_monitor_receipt_line from ..todos.summary_item import todo_text_content_revision @@ -30,22 +31,30 @@ def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id): content = {"state_file": str(state_file), "revision": "sha256:" + sha256(source_bytes).hexdigest()} # Only the writer's explicitly marked regions have generated ownership. - # Retain every other byte, including frontmatter, arbitrary headings, - # comments and historical narrative. Legacy/unmarked input stays a full + # Retain every other byte, except identity-bound historical Monitor + # receipt lines owned by their codec. Legacy/unmarked input stays a full # progressive read; malformed ownership fails rather than hiding text. lines = text.splitlines(keepends=True) - regions = find_todo_regions(lines) + visible = visible_markdown_lines(lines) + regions = find_todo_regions(lines, visible=visible) if regions and all(region.marked for region in regions): - parts, excluded, start = [], [], 0 + excluded, skipped = [], set() for region in regions: - parts.extend(lines[start:region.start]) excluded.append({"role": region.role, "start_line": region.start + 1, "end_line": region.end, "revision": "sha256:" + sha256( "".join(lines[region.start:region.end]).encode("utf-8")).hexdigest()}) - start = region.end - parts.extend(lines[start:]) - content.update(text="".join(parts), excluded_todo_regions=excluded) + skipped.update(range(region.start, region.end)) + receipts = [] + for index in sorted(visible - skipped): + if is_monitor_receipt_line(lines[index], goal_id=goal_id): + receipts.append({"start_line": index + 1, "end_line": index + 1, + "revision": "sha256:" + sha256(lines[index].encode("utf-8")).hexdigest()}) + skipped.add(index) + content.update(text="".join(line for index, line in enumerate(lines) if index not in skipped), + excluded_todo_regions=excluded) + if receipts: + content["excluded_monitor_receipts"] = receipts return content if source == "goal_acceptance": # The typed acceptance owner scopes task-binding observations while diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index 4876ff0969..d5ca780210 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -740,7 +740,7 @@ function completeGoalNarrative(content: JsonObject): boolean { || typeof content.text !== "string" || typeof content.state_file !== "string" || !content.state_file || typeof content.revision !== "string" || !ENVELOPED_SHA256_PATTERN.test(content.revision)) return false; let end = 0; - return content.excluded_todo_regions.every(value => { + if (!content.excluded_todo_regions.every(value => { const region = jsonObject(value); if (!region || !["user", "agent", "archive"].includes(String(region.role)) || !Number.isSafeInteger(region.start_line) || !Number.isSafeInteger(region.end_line) @@ -748,6 +748,19 @@ function completeGoalNarrative(content: JsonObject): boolean { || typeof region.revision !== "string" || !ENVELOPED_SHA256_PATTERN.test(region.revision)) return false; end = Number(region.end_line); return true; + })) return false; + if (!Object.hasOwn(content, "excluded_monitor_receipts")) return true; + if (!Array.isArray(content.excluded_monitor_receipts) || !content.excluded_monitor_receipts.length) return false; + end = 0; + return content.excluded_monitor_receipts.every(value => { + const region = jsonObject(value); + if (!region || !Number.isSafeInteger(region.start_line) || region.start_line !== region.end_line + || Number(region.start_line) <= end || typeof region.revision !== "string" + || !ENVELOPED_SHA256_PATTERN.test(region.revision) + || (content.excluded_todo_regions as JsonObject[]).some(todo => + Number(region.start_line) >= Number(todo.start_line) && Number(region.end_line) <= Number(todo.end_line))) return false; + end = Number(region.end_line); + return true; }); } @@ -809,7 +822,7 @@ export function projectInteractionWorkContext(request: JsonObject): JsonObject { pending.push(read); } else if (read.source === "goal_state") { if (completeGoalNarrative(content!)) sources.push({...read, - reason: `${String(read.reason ?? "")} This registered source delivers all text outside explicitly generated Todo regions inline, with full-file revision and excluded-region lineage; exact canonical Todo reads own task state/claims.`, + reason: `${String(read.reason ?? "")} This registered source delivers all text outside explicitly generated Todo regions inline, with full-file revision and excluded-region lineage; exact canonical Todo reads own task state/claims.${content!.excluded_monitor_receipts ? " Recognized historical Monitor receipt lines retain source lineage instead of repeating their encoded bodies; the original file remains available for receipt recovery." : ""}`, content: content!}); else { pending.push(read); diff --git a/tests/control_plane/test_goal_narrative_context.py b/tests/control_plane/test_goal_narrative_context.py index 107a74349d..39d61784e2 100644 --- a/tests/control_plane/test_goal_narrative_context.py +++ b/tests/control_plane/test_goal_narrative_context.py @@ -2,6 +2,7 @@ from __future__ import annotations from copy import deepcopy +import base64 from hashlib import sha256 import json import subprocess @@ -51,6 +52,70 @@ def section(role, heading, text): f"- [x] {text}\n\n") +def monitor_receipt(*, operation="monitor:previous", goal="context-goal"): + receipt = {"schema_version": "loopx_monitor_batch_receipt_v0", "goal_id": goal, + "operation_id": operation, "request_sha256": "a" * 64, + "writeback": {"schema_version": "monitor_poll_todo_writeback_v0", "goal_id": goal, + "monitor_effect_id": operation, "next_todos": [], "successor_receipts": [], + "todo_update": {"note": "Previous observation. " * 30_000}}} + encoded = base64.b64encode(json.dumps(receipt).encode()).decode() + return receipt, f"\n" + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_historical_monitor_receipts_keep_recovery_without_repeating_encoded_bodies(tmp_path, monkeypatch, provider): + from loopx.control_plane.scheduler.monitor_batch_receipts import _read_receipt + + runtime, registry, state, _todo = fixture(tmp_path, monkeypatch, provider) + receipt, marker = monitor_receipt() + before = "# Goal\nKeep every requirement.\n" + after = "\nFinal stop: do not publish.\n" + document = before + marker + section("archive", "Completed Work Archive", "Old observation.") + after + state.write_bytes(document.encode()) + packet = guard(runtime, registry) + channel = packet["interaction_contract"]["agent_channel"] + context = next(r["content"] for r in channel["work_context"]["sources"] if r.get("source") == "goal_state") + assert context["text"] == before + after + assert context["revision"] == "sha256:" + sha256(document.encode()).hexdigest() + assert context["excluded_monitor_receipts"] == [{"start_line": 3, "end_line": 3, + "revision": "sha256:" + sha256(marker.encode()).hexdigest()}] + assert len(json.dumps(context).encode()) < 1500 < len(document.encode()) + assert channel["work_context"]["complete"] and channel["delivery_allowed"] + assert not any(r.get("source") == "goal_state" for r in channel["required_reads"]) + assert state.read_bytes() == document.encode() + assert _read_receipt(state.read_text(), receipt["operation_id"]) == receipt + + +@pytest.mark.parametrize("change", ["foreign_goal", "wrong_identity", "unknown_schema", "invalid_body", + "extra_field", "duplicate_field", "trailing_prose", "fenced", "nested_comment"]) +def test_unowned_monitor_like_text_remains_verbatim(tmp_path, monkeypatch, change): + runtime, registry, state, _todo = fixture(tmp_path, monkeypatch, "sqlite") + receipt, marker = monitor_receipt(goal="different-goal" if change == "foreign_goal" else "context-goal") + if change == "wrong_identity": + marker = marker.replace(sha256(receipt["operation_id"].encode()).hexdigest(), "0" * 64) + elif change in {"unknown_schema", "extra_field", "duplicate_field"}: + if change == "unknown_schema": + receipt["schema_version"] = "future_receipt_v9" + if change == "extra_field": + receipt["authored_requirement"] = "Preserve this requirement." + body = json.dumps(receipt) + if change == "duplicate_field": + body = body[:-1] + ', "goal_id": "context-goal"}' + marker = marker[:marker.index(" ", 5) + 1] + base64.b64encode(body.encode()).decode() + " -->\n" + elif change == "invalid_body": + marker = f"\n" + elif change == "trailing_prose": + marker = marker.rstrip("\n") + " Keep the final requirement.\n" + elif change == "fenced": + marker = "```md\n" + marker + "```\n" + elif change == "nested_comment": + marker = "\n" + state.write_text("# Goal\n" + marker + section("agent", "Agent Todo", "Old task.") + "Final requirement.\n") + content = source(runtime, registry) + assert content["text"] == "# Goal\n" + marker + "Final requirement.\n" + assert "excluded_monitor_receipts" not in content + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_complete_narrative_and_canonical_work_reach_signed_host(tmp_path, monkeypatch, provider): runtime, registry, state, todo = fixture(tmp_path, monkeypatch, provider) diff --git a/tests/control_plane_ts/interaction_contract.test.ts b/tests/control_plane_ts/interaction_contract.test.ts index 48ebd90513..0a95892ef7 100644 --- a/tests/control_plane_ts/interaction_contract.test.ts +++ b/tests/control_plane_ts/interaction_contract.test.ts @@ -669,4 +669,13 @@ test("Goal narrative fulfillment needs complete source and ordered region lineag assert.deepEqual((rejected.work_context as JsonObject).sources ?? [], []); } assert.deepEqual(project({text: content.text}).required_reads, [read]); + const receipt = {start_line: 2, end_line: 2, revision: `sha256:${"c".repeat(64)}`}; + assert.deepEqual(project({...content, excluded_monitor_receipts: [receipt]}).required_reads, []); + for (const invalid of [[], [receipt, receipt], [{...receipt, start_line: 0, end_line: 0}], + [{...receipt, end_line: 3}], [{...receipt, start_line: 5, end_line: 5}], + [{...receipt, revision: "invalid"}]]) { + const rejected = project({...content, excluded_monitor_receipts: invalid}); + assert.deepEqual(rejected.required_reads, [read]); + assert.equal((rejected.work_context as JsonObject).complete, false); + } }); From 9f25edc8617686eb614750aeb4db16da961f5b5f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:21:16 +0800 Subject: [PATCH 34/40] fix(context): fit native SSH capture within the host prompt budget Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/heartbeat-automation-prompt.md | 11 ++++-- loopx/control_plane/heartbeat/builder.py | 1 + loopx/control_plane/heartbeat/task_body.py | 34 +++++++++++++++++++ .../test_codex_context_capture.py | 11 ++++-- .../test_heartbeat_envelope_transport.py | 31 +++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 085dad3812..4a832f0448 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -426,11 +426,12 @@ loopx-canary heartbeat-prompt \ `--context-delivery {off,on}` is experimental and defaults to `off`. It reuses existing scheduler-owned host declarations and the benchmark worker's explicit execution settings; it does not guess a host from process names or ambient native -session variables. Only two declared run modes can activate it: +session variables. These declared run modes can activate it: | Supported entry | Existing host declaration | | --- | --- | | Codex App heartbeat, same-session resume and recovery | `--codex-app`, equivalently `--runtime-profile codex_app_heartbeat` (or its complete explicit host tuple) | +| Interactive Codex App SSH Goal | `--runtime-profile codex_app_ssh_goal`; retains native `--begin-turn` and visible-Goal settlement | | Codex benchmark heartbeat worker | Existing `generic_cli` profile plus the worker-owned `LOOPX_EXECUTION_MODE=heartbeat` and `LOOPX_CONTEXT_DELIVERY=on` declarations; its execution owner rejects other modes | `--codex-app` is a shortcut for a complete runtime profile. `--host-surface` alone @@ -452,8 +453,8 @@ observation; directories alone never turn it on. This declaration check is not host authentication, execution authority or proof of retained input. Native delivery uses the explicit lifecycle assumption described below; final-input Host clients keep their independent retained-input confirmation contract. -Generic CLI, visible native Goals, Claude and other hosts remain outside this -opt-in. Harbor/container and installed desktop adoption retain their separate +Other generic CLI and native Goal profiles, Claude and other hosts remain outside +this opt-in. Harbor/container and installed desktop adoption retain their separate unqualified boundaries; listing an entry does not certify live adoption. For a trusted Codex host, render the native setup through the installed CLI. @@ -473,6 +474,10 @@ 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. Use one configuration for the declared Goal/Agent. A shared private root keeps different native sessions in separate journals, including interleaved sessions. +For an interactive SSH Goal, use `--runtime-profile codex_app_ssh_goal` in place +of `--codex-app` for both setup and the thin prompt. It retains the 4000-character +native Goal limit and the full command paths, with the same live contract and +native blocked/resume rules; it does not become a hosted heartbeat. Generate the normal bootstrap with `heartbeat-prompt --thin --codex-app --bootstrap`, its existing Goal/Agent/registry arguments, and diff --git a/loopx/control_plane/heartbeat/builder.py b/loopx/control_plane/heartbeat/builder.py index 36425f1df2..1698bdaa58 100644 --- a/loopx/control_plane/heartbeat/builder.py +++ b/loopx/control_plane/heartbeat/builder.py @@ -534,6 +534,7 @@ def build_heartbeat_prompt( brief_prompt_command=str(commands["brief_prompt_command"]), thin_prompt_command=str(commands["thin_prompt_command"]), **({"captured_envelope": True} if turn_envelope else {}), + **({"captured_context": True} if native_goal_host and context_delivery == "on" else {}), **reward_memory_rule_kwargs, ) task_body = bind_exact_turn_settlement_task_body( diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index f6252d508a..33c23280b4 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -438,6 +438,7 @@ def render_visible_goal_task_body( brief_prompt_command: str, thin_prompt_command: str, captured_envelope: bool = False, + captured_context: bool = False, ) -> str: del ( cli_preflight, @@ -461,6 +462,7 @@ def render_visible_goal_task_body( agent_scope_instruction=agent_scope_instruction, host_wait_rule=CODEX_NATIVE_GOAL_UNCHANGED_WAIT_RULE, captured_envelope=captured_envelope, + captured_context=captured_context, ) def render_traex_visible_goal_task_body( *, @@ -522,6 +524,7 @@ def _render_goal_task_body( host_wait_rule: str, work_rule: str = SCOPE_BOUNDED_WORK_RULE, captured_envelope: bool = False, + captured_context: bool = False, ) -> str: scope_block = f"\n{agent_scope_instruction}\n" if agent_scope_instruction else "" prequota_block = ( @@ -539,6 +542,37 @@ def _render_goal_task_body( "before selection, work or settlement. Retain transport flags on fresh guards.\n" if captured_envelope else "" ) + if captured_context: + # The opted-in native Goal has a 4000-character host limit, including + # the complete CLI/registry/runtime/capture paths. Keep those commands + # intact and refer to the registered active state instead of repeating + # its path. Ordinary/off Goal instructions remain byte-for-byte intact. + envelope_rule = ( + "Read complete same-invocation detail_ref.full_decision before " + "selection/work/settlement; honor envelope action/boundary/required_reads; " + "keep fresh-guard transport flags.\n" + if captured_envelope else "" + ) + wait_rule = ( + "\nNative `/goal` owns blocked state. Recheck quota at " + "scheduler_hint.unchanged_poll limit. Third identical blocked turn " + "without progress: update_goal(status=blocked); no spend/LoopX completion. " + "Only user `/goal resume` reactivates; rerun quota after resume." + if host_wait_rule == CODEX_NATIVE_GOAL_UNCHANGED_WAIT_RULE else host_wait_rule + ) + return f"""Goal `{goal_id}`: registered active state; {host_preamble} +{scope_block} +Current CLI `interaction_contract`; lifecycle/registry: `loopx-project`; runtime/projection drift: `loopx-self-repair`. +Follow user authority/repo rules. Protect credentials/private material; public-safe publication only. Destructive Git/production needs explicit authorization. Gate only affected paths; do independent allowed work. Reuse current authority; ask only for necessary missing authority. +{OPERATOR_LANGUAGE_RULE_THIN} +{prequota_block}Each iteration: read complete successful JSON: +`{quota_guard_command}` +{envelope_rule}Use selection_command if required; follow reentry; do/verify authorized work. Settle via current settlement_plan.ordered_steps, else next_cli_actions. Exact identities/effect ids; actual outcomes. Never reconstruct static refresh/spend; ambiguous effects: readback/recovery, no blind retry. Preserve flags; truthful evidence. Authorized entrypoint repair; unavailable/incomplete contract: no work/spend/completion claim. + +Read agent_channel.work_context + required_reads (or envelope) before work. Size by task/evidence/risk within authority/budget, never ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, never wake counts. Continue feasible integration/negative cases/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: retain original/cumulative outcome requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project guide). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. +Notification=output only. Progress alone never creates a host Goal. After settlement: recheck quota; obey continuation/wait, no unchanged polls. Complete {completion_subject} only on should_run=false + terminal no-follow-up; else next action/wait.{wait_rule} + +{policy_tail}""" return f"""Advance goal `{goal_id}` from `{active_state}` {host_preamble} {scope_block} diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 0f03790925..02d7a31da9 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -91,6 +91,10 @@ def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, ho # Real worker qualification follows returned cursors; output budgets can # shrink a page, so precomputed fixed offsets are not its production path. bounded_pages = bounded_pages or host == "benchmark" + # These shapes retain complete, independently checked reader pages in the + # native-selected replay. Other shapes below deliberately exercise reset + # and exact-original recovery instead of claiming qualified reuse. + qualified_resume = host == "benchmark" or ssh_goal or (adaptive and bounded_pages) isolate_sqlite_runtime(tmp_path, monkeypatch) binary, env, workspace, home = _native_fixture(tmp_path) env.pop("CODEX_ROLLOUT_TRACE_ROOT", None) @@ -296,7 +300,7 @@ def response(request, number, message): if phase == "resuming": if not resume_loss: assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") - if host == "benchmark" or ssh_goal: + if qualified_resume: # Actual worker process exit/resume, qualified by the shared # native-selected replay checkpoint. No old full prefetch. assert ("LoopX context baseline invalidated." in _input_text(request)) == bool(resume_loss) @@ -602,7 +606,10 @@ def held(*args, **kwargs): current = prepare_context_delivery(json.loads(projection.read_bytes()), scope={k: wires["warm"]["target"][k] for k in ("goal_id", "agent_id", "session_id", "context_epoch")}, capture_id=wires["warm"]["target"]["capture_id"], surface=surface) baseline = wires["cold"] - if resumed and host != "benchmark" and not ssh_goal: + if resumed and qualified_resume and not resume_loss: + assert wires["warm"]["base"] == baseline["target"] + assert current["target"]["context_epoch"] == baseline["target"]["context_epoch"] + elif resumed and not qualified_resume: # This path independently reread every original byte after a reset. # Bind those verified bytes to the new native epoch, rather than letting # automatic full fallback conceal an obsolete pre-loss baseline. diff --git a/tests/control_plane/test_heartbeat_envelope_transport.py b/tests/control_plane/test_heartbeat_envelope_transport.py index 89a1ce4136..7a853a5331 100644 --- a/tests/control_plane/test_heartbeat_envelope_transport.py +++ b/tests/control_plane/test_heartbeat_envelope_transport.py @@ -440,3 +440,34 @@ def test_ssh_goal_capture_keeps_native_begin_turn_and_off_boundary(tmp_path, tur if turn_envelope: assert "--turn-envelope" in guard assert "detail_ref.full_decision" in enabled["task_body"] + + +@pytest.mark.parametrize("turn_envelope", [False, True]) +def test_ssh_goal_capture_fits_host_with_full_command_paths(tmp_path, turn_envelope): + # Fixed physical path width qualifies the shipped command workload on every + # OS; the native cases also retain their actual, unshortened pytest paths. + with stable_budget_fixture_root(tmp_path) as base: + root = (base / ("p" * (160 - len(str(base.resolve())) - 1))).resolve() + root.mkdir() + paths = {"registry_path": root / "project/.loopx/registry.json", + "runtime_root": root / "runtime", "cli_bin": str(Path(sys.executable).with_name("loopx"))} + args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile="codex_app_ssh_goal", + thin=True, **paths) + ordinary = build_heartbeat_prompt(**args) + assert ordinary == build_heartbeat_prompt(**args, context_delivery="off") + enabled = build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=root, + codex_context_root=str(root), turn_envelope=turn_envelope) + body, command = enabled["task_body"], shlex.split(enabled["quota_guard_command"]) + assert enabled["interface_budget"]["max_chars"] == 4000 and len(body) <= 4000 + for option, value in (("--registry", paths["registry_path"]), + ("--runtime-root", paths["runtime_root"]), + ("--decision-output-root", root), ("--codex-context-root", root)): + assert command[command.index(option) + 1] == str(value) + assert command[0] == paths["cli_bin"] + assert "--begin-turn" in command and "--heartbeat-turn-receipt" not in command + assert "--context-delivery" in command and "LOOPX_TURN=" not in body + if turn_envelope: + assert "detail_ref.full_decision" in body + with pytest.raises(ValueError, match="4000-character"): + build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=root, + codex_context_root=str(root), turn_envelope=turn_envelope, agent_scopes=[c * 320 for c in "stu"]) From 65ef62d732b6e42407ede941801208b83b128b03 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:30:26 +0800 Subject: [PATCH 35/40] refactor(context): reuse the shared digest shape in context readers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/chat_history.py | 5 ++-- .../scheduler/monitor_batch_receipts.py | 3 ++- .../test_content_digest_single_owner.py | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/loopx/chat_history.py b/loopx/chat_history.py index 18322330fc..a755ba1fb5 100644 --- a/loopx/chat_history.py +++ b/loopx/chat_history.py @@ -4,9 +4,10 @@ import base64 import hashlib import json -import re from typing import Any +from .control_plane.content_digest import BARE_SHA256_PATTERN + def page_chat_messages(session_id: str, messages: list[dict[str, Any]], *, limit: int, cursor: str | None = None) -> dict[str, Any]: @@ -25,7 +26,7 @@ def page_chat_messages(session_id: str, messages: list[dict[str, Any]], *, if (source != session_id or type(offset) is not int or type(total) is not int or not 0 < offset < total <= len(messages) or not isinstance(expected_digest, str) - or not re.fullmatch(r"[a-f0-9]{64}", expected_digest)): + or not BARE_SHA256_PATTERN.fullmatch(expected_digest)): raise ValueError() except (ValueError, TypeError, UnicodeDecodeError) as exc: raise ValueError("invalid history cursor") from exc diff --git a/loopx/control_plane/scheduler/monitor_batch_receipts.py b/loopx/control_plane/scheduler/monitor_batch_receipts.py index 76087cf245..0802e18413 100644 --- a/loopx/control_plane/scheduler/monitor_batch_receipts.py +++ b/loopx/control_plane/scheduler/monitor_batch_receipts.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import Any +from ..content_digest import BARE_SHA256_PATTERN from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result from ..todos.path_resolution import resolve_todo_state_path @@ -69,7 +70,7 @@ def unique_object(pairs): and isinstance(operation, str) and bool(operation) and hashlib.sha256(operation.encode()).hexdigest() == match[1] and isinstance(receipt.get("request_sha256"), str) - and re.fullmatch(r"[0-9a-f]{64}", receipt["request_sha256"]) is not None + and BARE_SHA256_PATTERN.fullmatch(receipt["request_sha256"]) is not None and isinstance(writeback, dict) and writeback.get("schema_version") == "monitor_poll_todo_writeback_v0" and writeback.get("goal_id") == goal_id diff --git a/tests/architecture/test_content_digest_single_owner.py b/tests/architecture/test_content_digest_single_owner.py index d71af03090..0acf6a3efa 100644 --- a/tests/architecture/test_content_digest_single_owner.py +++ b/tests/architecture/test_content_digest_single_owner.py @@ -177,6 +177,7 @@ "loopx.capabilities.periodic_report.machine_defaults", "loopx.capabilities.progress_review.receipt", "loopx.chat_action_normalization", + "loopx.chat_history", "loopx.cli_commands.quota_capture", "loopx.configuration_transaction", "loopx.control_plane.agents.execution_facts", @@ -195,6 +196,7 @@ "loopx.control_plane.goals.deletion_service", "loopx.control_plane.goals.goal_amendment_proposal", "loopx.control_plane.projects.registry_codec", + "loopx.control_plane.scheduler.monitor_batch_receipts", "loopx.control_plane.status.goal_source", "loopx.control_plane.testing.cli_output_semantics", "loopx.control_plane.testing.release_commit_qualification", @@ -222,6 +224,10 @@ # A consumer that hands `re` a pattern this file cannot fold. Folding reads # every construction except the declared readers, each fenced below. UNREADABLE_CONSUMER_CONSTRUCTIONS: dict[str, str] = { + "loopx.control_plane.scheduler.monitor_batch_receipts": ( + "_read_receipt locates an escaped operation-bound HTML receipt marker, " + "not a whole-value digest. Its sole dynamic regex is fenced below." + ), "loopx.capabilities.benchmark_toolkit.integrity": ( "_marker_present matches an escaped caller-supplied filename/token inside " "command text with explicit basename boundaries; it does not validate a " @@ -1270,6 +1276,24 @@ def test_cli_output_dynamic_prose_pattern_is_fenced_to_its_literal_question() -> assert isinstance(literal, ast.Constant) and isinstance(literal.value, str) and literal.value +def test_monitor_receipt_dynamic_pattern_only_locates_its_operation_marker() -> None: + dotted = "loopx.control_plane.scheduler.monitor_batch_receipts" + module = importlib.import_module(dotted) + tree = ast.parse(Path(module.__file__).read_text(encoding="utf-8")) + reader = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "_read_receipt") + calls = [node for node in ast.walk(reader) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and ast.unparse(node.func) == "re.findall"] + assert len(calls) == 1 + unreadable = [site for site in _module_of(dotted)["constructions"] if not site["readable"]] + assert [site["line"] for site in unreadable] == [calls[0].lineno] + pattern = calls[0].args[0] + assert isinstance(pattern, ast.BinOp) and isinstance(pattern.op, ast.Add) + assert ast.unparse(pattern.left) == "re.escape(marker)" + assert isinstance(pattern.right, ast.Constant) and pattern.right.value == r"([A-Za-z0-9+/=]+) -->" + assert any(isinstance(node, ast.Assign) and ast.unparse(node) == "marker = _receipt_marker(operation_id)" + for node in reader.body) + + # --- layer 4: behaviour, per spelling and per surface ------------------------------------- From 30bad014a6451c2b99aad19632a6b6965c16c33a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:48:16 +0800 Subject: [PATCH 36/40] fix(context): bound complete native guard presentations Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 13 ++-- loopx/cli_commands/quota.py | 5 +- loopx/cli_commands/quota_capture.py | 64 +++++++++++-------- .../test_codex_context_capture.py | 16 ++++- .../test_codex_context_lifecycle.py | 17 +++++ .../test_saved_context_project.py | 12 +++- 6 files changed, 89 insertions(+), 38 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index b4cb4125da..d94c7a0293 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -231,7 +231,9 @@ setting. Ordinary machine validation still applies to business operations. 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 stays at 3 KiB. Continuations +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 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, @@ -427,7 +429,8 @@ Codex App SSH Goal(`--runtime-profile codex_app_ssh_goal`),以及现有 Co SSH Goal 使用 `--runtime-profile codex_app_ssh_goal` 生成 context-config 和 thin/bootstrap 提示, 沿用可见 Goal 的执行与结算身份;仅启用差分时接入捕获目录,不转换成自动心跳。 原生 `quota should-run` 与已保存捕获的 `context-project` 共用分页呈现,直接返回第零页、 -固定摘要的后续读取与完整恢复,不另行准入。内联页保持 3 KiB;后续命令最多请求 +固定摘要的后续读取与完整恢复,不另行准入。内联正文最多 3 KiB,必要时缩小,使包含 +恢复字段与完整指引的 guard stdout 不超过 7900 UTF-8 字节;后续命令最多请求 8 KiB 正文,并用 `--max-page-output-bytes 7900` 将成功 JSON stdout(含转义、 后续命令与换行)限制为 7900 字节。先消费内联页,再跟随 next_command;保留历史 上限未知时,每次只读一个有界输出。该上限不证明模型保留。Code Mode 直接转发 @@ -499,7 +502,8 @@ 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 -to leave room for projection metadata. Subsequent native commands request up to +and shrinks against the final presentation's 7900-byte stdout limit, including +recovery, activation and instruction metadata. 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 @@ -522,7 +526,8 @@ input retention. 原始摘要和 Turn 回执,不重新准入或扣减额度。 开启时,成功的已保存投影同时用现有 `context_capture_page_v1` 字段返回第一页。 -该页内容最多 3 KiB,为投影元数据留出输出空间;后续原生命令最多请求 8 KiB +该页内容最多 3 KiB,按最终呈现的 7900 字节 stdout 上限缩小,包含恢复、启用配置与 +完整指引元数据;后续原生命令最多请求 8 KiB 正文,成功 JSON stdout 限制为 7900 字节。可选的 JSON 专用参数 `--max-page-output-bytes` 按 UTF-8 边界缩减正文,并在 `next_command` 中保留。 上限不足时返回错误,不返回无进展的分页;增大上限后重读同一捕获。省略该参数 diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index 5dadd7f85d..7198d5f03d 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -540,9 +540,8 @@ def _emit_quota_result( surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet", capture_id=capture_id, continuity_generation=getattr(args, "_context_delivery_generation", None)) recovery = payload.get("recovery") - view = native_context_capture_view(payload, capture_directory, context_directory=directory) - payload = {"ok": True, **view, "capture_sha256": capture_id, - **({"recovery": recovery} if recovery else {})} + payload = native_context_capture_view(payload, capture_directory, context_directory=directory, + metadata={"capture_sha256": capture_id, **({"recovery": recovery} if recovery else {})}) 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 ce91fc9bc3..e5e48a9c0e 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -33,7 +33,9 @@ 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) -> dict[str, Any]: + output_limit: int | None, instruction: bool, + presentation: dict[str, Any] | None = None, + next_page_bytes: int | None = None) -> dict[str, Any]: end = min(offset + size, len(raw)) while end < len(raw) and raw[end] & 0xC0 == 0x80: end -= 1 @@ -41,13 +43,13 @@ def _capture_page(path: Path, raw: bytes, digest: str, *, offset: int, size: int def page(text: str) -> dict[str, Any]: cursor = offset + len(text.encode("utf-8")) - result: dict[str, Any] = {"ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, + result: dict[str, Any] = {**(presentation or {}), "ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, "offset": offset, "next_offset": cursor if cursor < len(raw) else None, "total_bytes": len(raw), "eof": cursor == len(raw), "content": text} if instruction: result["read_instruction"] = CONTEXT_CAPTURE_PAGE_GUIDANCE if cursor < len(raw): - result["next_command"] = context_capture_read_command(path, digest, offset=cursor, page_bytes=size, + result["next_command"] = context_capture_read_command(path, digest, offset=cursor, page_bytes=next_page_bytes or size, max_page_output_bytes=output_limit) return result @@ -75,7 +77,8 @@ def fits(result: dict[str, Any]) -> bool: return best -def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: +def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, Any] | None = None, + next_page_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 @@ -109,7 +112,8 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: if offset > len(raw) or (offset < len(raw) and raw[offset] & 0xC0 == 0x80): 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)) + output_limit=output_limit, instruction=bool(context_dir), + presentation=presentation, next_page_bytes=next_page_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. @@ -119,7 +123,8 @@ def read_context_capture(args: argparse.Namespace) -> dict[str, Any]: def native_context_capture_view(projected: dict[str, Any], directory: Path, *, - context_directory: str | None = None) -> dict[str, Any]: + context_directory: str | None = None, + metadata: dict[str, Any] | 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 @@ -137,31 +142,36 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, reference = {key: projected["recovery"][key] for key in ("path", "sha256")} 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) - result = {"mode": mode, **reference, **({"target": target} if target else {})} - page = read_context_capture(argparse.Namespace(capture_file=reference["path"], - capture_sha256=reference["sha256"], offset=0, page_bytes=3072)) - if page["ok"]: - if not page["eof"]: - page["next_command"] = context_capture_read_command(Path(reference["path"]), reference["sha256"], - offset=page["next_offset"], page_bytes=NATIVE_CONTEXT_PAGE_BYTES, - max_page_output_bytes=NATIVE_CONTEXT_PAGE_OUTPUT_BYTES) - result.update(page) - from ..control_plane.turn_driver.codex_context import register_native_context_view - - register_native_context_view(context_directory, - {**result, "ancestry": projected.get("ancestry", 0)}) + result = {"ok": True, **(metadata or {}), "mode": mode, **reference, + **({"target": target} if target else {})} # 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. presentation = ("This is the full current capture; after complete reading, continue from it." if mode == "full" else "Read the current continuity basis and instruction before its change tree.") - result["instruction"] = ( + instruction = ( "Presentation of the original decision only; authority and freshness are unchanged. " - + presentation + " " - + ("This result includes page zero; consume it before next_command. command restarts " - "at zero if the inline page is incomplete or unsupported. " if page["ok"] else "") - + CONTEXT_CAPTURE_PAGE_GUIDANCE - ) + + presentation + " ") + result["instruction"] = (instruction + "This result includes page zero; consume it before next_command. " + "command restarts at zero if the inline page is incomplete or unsupported. " + CONTEXT_CAPTURE_PAGE_GUIDANCE) + # Bound the complete stdout, including caller-owned recovery and activation + # fields. Capping only content leaves the guard itself vulnerable to native + # truncation before the consumer can discover its continuation command. + 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) + if page["ok"]: + result = page + from ..control_plane.turn_driver.codex_context import register_native_context_view + + register_native_context_view(context_directory, + {**result, "ancestry": projected.get("ancestry", 0)}) + else: + result["instruction"] = instruction + CONTEXT_CAPTURE_PAGE_GUIDANCE + if len(json.dumps(result, ensure_ascii=False, separators=(",", ":")).encode()) + 1 > NATIVE_CONTEXT_PAGE_OUTPUT_BYTES: + raise QuotaDecisionTransportError("Native context metadata exceeds the output limit; " + "read the saved original decision and receipt without rerunning admission.") return result @@ -224,7 +234,9 @@ def project_saved_context_capture(args: argparse.Namespace) -> dict[str, Any]: capture_id=hashlib.sha256(raw).hexdigest(), continuity_generation=getattr(args, "_context_delivery_generation", None)) native_prepared = True - native_view = native_context_capture_view(projected, cache, context_directory=directory) + native_view = native_context_capture_view(projected, cache, context_directory=directory, + metadata={"recovery": full_reference, "capture_sha256": args.capture_sha256, + "context_delivery": activation}) 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: diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index 02d7a31da9..a81f205ea9 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -221,7 +221,7 @@ 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])) + " && printf WIRE_READY" + return rendered + " > " + shlex.quote(str(wire_paths[name])) + " && cat " + shlex.quote(str(wire_paths[name])) def pages(name, path): raw = path.read_bytes() @@ -322,7 +322,13 @@ def response(request, number, message): cmd = invoke("warm") elif phase in {"cold_ready", "warm_ready"}: name = phase.split("_")[0] - descriptor = json.loads(wire_paths[name].read_text()) + # The native consumer must discover the next command from its + # actual retained input. Disk reads below are independent expected + # byte/state oracles, never a substitute for a truncated guard. + 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 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. @@ -335,8 +341,11 @@ def response(request, number, message): capture = Path(wires[name]["recovery"]["path"]).parent / "decision.json" snapshots.append((capture, capture.read_bytes())) read_paths[name] = Path(wires[name]["recovery"]["path"]) if wires[name]["mode"] == "full" else wire_file - stages["pages"] = pages(name, read_paths[name]) + stages["pages"] = ([descriptor["next_command"]] if not descriptor["eof"] else []) if bounded_pages else pages(name, read_paths[name]) + page_digests[name] = descriptor["sha256"] stages["phase"] = name + "_read" + if not stages["pages"]: + return response(request, number, message) cmd = stages["pages"].pop(0) elif phase == "cold_read": assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") @@ -511,6 +520,7 @@ def held(*args, **kwargs): "components": [{"id": "task", "source_path": str(state)}], "point_metadata": {}})) env["LOOPX_CHECKPOINT_REQUEST"] = str(declaration) result = run_once(env) + assert not errors, errors assert result["ok"] and result["host_invoked"], result assert result["context_transport"] == {"available": True} assert result["session"]["session_id"] diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py index e6ee3aadad..163fcbbc0f 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -1,6 +1,7 @@ """Native delivery assumptions, invalidation, recovery and scope isolation.""" from __future__ import annotations +import hashlib import json import os from pathlib import Path @@ -200,6 +201,22 @@ def test_native_page_zero_exposes_current_continuity_and_preserves_full_recovery assert (tmp_path / "session.json").read_bytes() == before +def test_native_page_zero_without_metadata_room_retains_original_recovery(tmp_path, monkeypatch): + from loopx.cli_commands import quota_capture + from loopx.control_plane.quota.error_codes import QuotaDecisionTransportError + + setup(tmp_path, monkeypatch) + _, wire = project(tmp_path, payload={"ok": True, "requirements": "完整要求🙂" * 1000}) + before = context._state(tmp_path) + monkeypatch.setattr(quota_capture, "NATIVE_CONTEXT_PAGE_OUTPUT_BYTES", 1) + with pytest.raises(QuotaDecisionTransportError, match="saved original decision"): + quota_capture.native_context_capture_view(wire, Path(wire["recovery"]["path"]).parent, + context_directory=str(tmp_path)) + assert context._state(tmp_path) == before + raw = Path(wire["recovery"]["path"]).read_bytes() + assert hashlib.sha256(raw).hexdigest() == wire["recovery"]["sha256"] + + @pytest.mark.parametrize("mode", ["full", "delta"]) def test_resume_requires_complete_original_read_before_reusing_baseline(tmp_path, monkeypatch, mode): event = setup(tmp_path, monkeypatch) diff --git a/tests/control_plane/test_saved_context_project.py b/tests/control_plane/test_saved_context_project.py index 0e6643b227..b5a818eeb0 100644 --- a/tests/control_plane/test_saved_context_project.py +++ b/tests/control_plane/test_saved_context_project.py @@ -144,10 +144,14 @@ def test_saved_projection_manages_private_cache_without_authority_io(tmp_path, m @pytest.mark.parametrize("body", ["Exact requirement.\n" * 700, "完整要求与来源🙂\n" * 700], ids=["ascii", "utf8"]) -def test_saved_projection_first_page_continues_exact_bytes_and_keeps_restart(tmp_path, monkeypatch, body): +@pytest.mark.parametrize("deep_path", [False, True], ids=["short", "deep"]) +def test_saved_projection_first_page_continues_exact_bytes_and_keeps_restart(tmp_path, monkeypatch, body, deep_path): from loopx.control_plane.turn_driver.codex_context import codex_context_hook from loopx.control_plane.turn_driver.codex_input_trace import _capture_page_range + if deep_path: + tmp_path = tmp_path / ("capture-location-" * 9) / ("original-context-" * 9) + tmp_path.mkdir(parents=True) context = tmp_path / "native" context.mkdir(mode=0o700) monkeypatch.setenv("CODEX_THREAD_ID", "native-session") @@ -164,6 +168,9 @@ def test_saved_projection_first_page_continues_exact_bytes_and_keeps_restart(tmp "--capture-sha256", digest, "--goal-id", "goal", "--agent-id", "agent", "--turn-instance-id", "turn", "--context-delivery", "on", "--codex-app", "--codex-context-dir", str(context), cwd=tmp_path) assert code == 0 and result["mode"] == "full", result + # Include the final recovery, activation and instruction fields: an inline + # content cap alone does not bound the actual native guard stdout. + assert len(json.dumps(result, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 7900 delivered = Path(result["path"]).read_bytes() delivered_digest = hashlib.sha256(delivered).hexdigest() assert result["sha256"] == delivered_digest and json.loads(delivered) == json.loads(raw) @@ -175,6 +182,7 @@ def test_saved_projection_first_page_continues_exact_bytes_and_keeps_restart(tmp argv = shlex.split(page["next_command"]) code, page = _run_cli(registry, runtime, *argv[argv.index("quota"):], cwd=tmp_path) assert code == 0 and page["offset"] == len(combined) + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= 7900 combined += page["content"].encode() assert combined == delivered and hashlib.sha256(combined).hexdigest() == delivered_digest argv = shlex.split(result["command"]) @@ -212,7 +220,7 @@ def fail_optional_wire(path, payload, **kwargs): monkeypatch.setattr(decision_capture, "write_decision_capture", fail_optional_wire) if failure == "inline_read": from loopx.cli_commands import quota_capture - monkeypatch.setattr(quota_capture, "read_context_capture", lambda args: {"ok": False}) + monkeypatch.setattr(quota_capture, "read_context_capture", lambda args, **kwargs: {"ok": False}) digest = hashlib.sha256(raw).hexdigest() args = build_parser().parse_args(["--format", "json", "quota", "context-project", "--capture-file", str(path), "--capture-sha256", digest, "--goal-id", "saved-goal", From ccb3830926b20fa18403e41b3e4d715a233698c6 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:52:29 +0800 Subject: [PATCH 37/40] fix(context): verify batched native Code Mode reader emissions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 4 ++- .../turn_driver/codex_context_history.py | 34 +++++++++++++------ .../test_codex_context_resume.py | 27 +++++++++++---- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index d94c7a0293..c1aa676d2f 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -286,7 +286,8 @@ baseline's reader interval and subsequent records; unrelated earlier history is neither hashed nor decoded, and total file size does not decide reuse. The session metadata's creator version is not the running host version. Code-mode parent emissions must retain the same complete page bytes observed by nested -reader hooks. Missing or changed evidence and unqualified replay records fall +reader hooks. Several nested reads may share one parent call; their individual +emission digests and full byte coverage remain required. Missing or changed evidence and unqualified replay records fall back to full. One decoded record remains bounded; this is not a history-size limit. Legacy whole-prefix checkpoints require a fresh complete read. It retains the normal-read engineering assumption rather than certifying future model input; @@ -460,6 +461,7 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 rollout 上验证;流式读取当前基线对应区间及后续记录,不哈希或解码无关旧历史, 文件总大小不决定是否差分。session metadata 的创建版本不等于当前宿主版本。 code-mode 父调用的保留输出须包含嵌套 reader hook 观察到的全部精确页字节。 +同一父调用可以包含多次嵌套读取,仍逐份核对其输出摘要和全部字节覆盖。 证据缺失、改写或重放记录不受支持时回退全量;单条记录的解码内存仍有边界, 不是历史文件容量限制。旧版全前缀检查点须重新完整读取后建立新检查点。仍采用正常读取后的 工程假设,不声称证明未来模型输入;不扫描模型请求,也不要求模型 ACK。 diff --git a/loopx/control_plane/turn_driver/codex_context_history.py b/loopx/control_plane/turn_driver/codex_context_history.py index 016b6871f5..6ac6e86bc6 100644 --- a/loopx/control_plane/turn_driver/codex_context_history.py +++ b/loopx/control_plane/turn_driver/codex_context_history.py @@ -178,21 +178,33 @@ def _matches(anchor: dict[str, Any], snapshot: dict[str, Any], stream: BinaryIO, call = payload["call_id"] pages = _read_pages(payload.get("output")) digest = _digest(pages) + groups = [pages] if call in required: if call in seen or digest != required[call]: return False seen.add(call) - elif payload.get("type") != "custom_tool_call_output" or digest not in expected_pages: - pages = [] - if pages: - seen_pages[digest] += 1 - for page in pages: - if page.get("sha256") != reference["sha256"]: - continue - span = capture_page_range(page, raw=body, digest=reference["sha256"]) - if span is None: - return False - ranges.append(span) + elif payload.get("type") == "custom_tool_call_output": + output = payload.get("output") + # One native Code Mode parent can retain several emissions + # from independently observed nested reads. Preserve those + # emission boundaries instead of hashing their concatenation + # as if it were one nested read. Every exact group and byte + # still has to be present; duplicate groups do not help. + if isinstance(output, list): + groups = [_read_pages(item.get("text")) for item in output + if isinstance(item, dict) and item.get("type") == "input_text"] + groups = [group for group in groups if group and _digest(group) in expected_pages] + else: + groups = [] + for group in groups: + seen_pages[_digest(group)] += 1 + for page in group: + if page.get("sha256") != reference["sha256"]: + continue + span = capture_page_range(page, raw=body, digest=reference["sha256"]) + if span is None: + return False + ranges.append(span) if not _append_only(record, event): return False cursor = 0 diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index 6f2d6c2c6e..522dce22bb 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -14,7 +14,7 @@ def append(path, kind, payload): def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost_prefix=False, - history_prefix_mib=0, version="0.162.1", code_mode=False): + history_prefix_mib=0, version="0.162.1", code_mode=False, parent_batch=1): event = setup(root, monkeypatch) history = root / "rollout.jsonl" append(history, "session_meta", {"id": "session", "cwd": str(root), @@ -32,6 +32,7 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost "heartbeat_receipt": {"turn_instance_id": "first"}} _, first = project(root, payload=payload) pages = read_pages(root, first) + emissions = [] for i, page in enumerate(pages if complete else pages[:-1]): call = f"reader-{i}" body = json.dumps(page) @@ -40,8 +41,13 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost if code_mode: # Native code-mode retains the parent emission; the nested hook # has a different call id. Its actual output must still cover all bytes. - append(history, "response_item", {"type": "custom_tool_call_output", "call_id": f"parent-{i}", - "output": [{"type": "input_text", "text": output}]}) + if parent_batch == 1: + append(history, "response_item", {"type": "custom_tool_call_output", "call_id": f"parent-{i}", + "output": [{"type": "input_text", "text": output}]}) + else: + if not emissions: + append(history, "response_item", {"type": "custom_tool_call", "call_id": f"parent-{i}"}) + emissions.append({"type": "input_text", "text": output}) else: append(history, "response_item", {"type": "function_call_output", "call_id": call, "output": output}) if lost_prefix and i == len(pages) - 1: @@ -50,6 +56,12 @@ def observed_read(root, monkeypatch, *, complete=True, shell_wrapper=False, lost history.write_text("".join(json.dumps(row) + "\n" for row in rows)) context.codex_context_hook(str(root), {**event, "hook_event_name": "PostToolUse", "tool_use_id": call, "tool_response": body}) + if emissions and (len(emissions) == parent_batch or i == len(pages) - 1): + # Nested read hooks run before the enclosing Code Mode emits its + # complete result. Preserve every emission in that native parent. + append(history, "response_item", {"type": "custom_tool_call_output", "call_id": f"parent-{i}", + "output": emissions}) + emissions = [] return event, history, payload @@ -167,11 +179,14 @@ def test_old_version_can_resume_current_verified_reader_format(tmp_path, monkeyp @pytest.mark.parametrize("lose_page", [False, True]) -def test_code_mode_retained_emissions_need_complete_native_read_coverage(tmp_path, monkeypatch, lose_page): - event, history, payload = observed_read(tmp_path, monkeypatch, code_mode=True) +@pytest.mark.parametrize("parent_batch", [1, 2]) +def test_code_mode_retained_emissions_need_complete_native_read_coverage(tmp_path, monkeypatch, lose_page, parent_batch): + event, history, payload = observed_read(tmp_path, monkeypatch, code_mode=True, parent_batch=parent_batch) if lose_page: rows = [json.loads(line) for line in history.read_text().splitlines()] - next(row for row in rows if row["type"] == "response_item")["payload"]["output"] = [] + outputs = [row for row in rows if row["type"] == "response_item" + and row["payload"].get("type") == "custom_tool_call_output"] + outputs[-1]["payload"]["output"] = outputs[-1]["payload"]["output"][1:] history.write_text("".join(json.dumps(row) + "\n" for row in rows)) context.codex_context_hook(str(tmp_path), {**event, "source": "resume"}) _, wire = project(tmp_path, "second", payload=payload) From 8a06a8ff5fa2bf60d7e3658618e50818c242b0b1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:59:44 +0800 Subject: [PATCH 38/40] fix(context): fit long installed CLI routes in native Goal prompts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/heartbeat/task_body.py | 18 ++++++------- .../test_heartbeat_envelope_transport.py | 25 +++++++++++++------ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index 33c23280b4..65e737b803 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -550,27 +550,27 @@ def _render_goal_task_body( envelope_rule = ( "Read complete same-invocation detail_ref.full_decision before " "selection/work/settlement; honor envelope action/boundary/required_reads; " - "keep fresh-guard transport flags.\n" + "keep fresh-guard flags.\n" if captured_envelope else "" ) wait_rule = ( - "\nNative `/goal` owns blocked state. Recheck quota at " + "\n`/goal` owns blocked state. Recheck quota at " "scheduler_hint.unchanged_poll limit. Third identical blocked turn " "without progress: update_goal(status=blocked); no spend/LoopX completion. " - "Only user `/goal resume` reactivates; rerun quota after resume." + "Only user `/goal resume` reactivates; then rerun quota." if host_wait_rule == CODEX_NATIVE_GOAL_UNCHANGED_WAIT_RULE else host_wait_rule ) - return f"""Goal `{goal_id}`: registered active state; {host_preamble} + return f"""`{goal_id}`: registered active state; {host_preamble} {scope_block} -Current CLI `interaction_contract`; lifecycle/registry: `loopx-project`; runtime/projection drift: `loopx-self-repair`. -Follow user authority/repo rules. Protect credentials/private material; public-safe publication only. Destructive Git/production needs explicit authorization. Gate only affected paths; do independent allowed work. Reuse current authority; ask only for necessary missing authority. +Current CLI interaction_contract; lifecycle/registry: loopx-project; runtime/projection drift: loopx-self-repair. +Follow user authority/repo rules. Protect credentials/private material; publish public-safe only. Destructive Git/production needs explicit permission. Gate affected paths only; do independent authorized work. Reuse current authority; ask only for necessary missing authority. {OPERATOR_LANGUAGE_RULE_THIN} {prequota_block}Each iteration: read complete successful JSON: `{quota_guard_command}` -{envelope_rule}Use selection_command if required; follow reentry; do/verify authorized work. Settle via current settlement_plan.ordered_steps, else next_cli_actions. Exact identities/effect ids; actual outcomes. Never reconstruct static refresh/spend; ambiguous effects: readback/recovery, no blind retry. Preserve flags; truthful evidence. Authorized entrypoint repair; unavailable/incomplete contract: no work/spend/completion claim. +{envelope_rule}Follow required selection_command/reentry; do/verify authorized work. Settle current settlement_plan.ordered_steps, else next_cli_actions. Exact identities/effect ids; actual outcomes. Never rebuild static refresh/spend; ambiguous effects: readback/recovery, no blind retry. Keep flags; truthful evidence. Authorized entrypoint repair; unavailable/incomplete contract: no work/spend/completion claim. -Read agent_channel.work_context + required_reads (or envelope) before work. Size by task/evidence/risk within authority/budget, never ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, never wake counts. Continue feasible integration/negative cases/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: retain original/cumulative outcome requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project guide). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. -Notification=output only. Progress alone never creates a host Goal. After settlement: recheck quota; obey continuation/wait, no unchanged polls. Complete {completion_subject} only on should_run=false + terminal no-follow-up; else next action/wait.{wait_rule} +Read agent_channel.work_context+required_reads (or envelope) before work. Size by task/evidence/risk within authority/budget, not ops/files/wakes; calls/writeback ≠ completion. Correct locally; obey projected replan/stop, not wake counts. Continue feasible integration/negatives/readback; keep recovery/resume evidence+next action. Material user delivery/corrections: keep original/cumulative requirements; reuse inline goal_acceptance (inspect only if absent), then refine. Milestones: review artifacts/evidence; batch refresh-state --goal-outcome-update-json (loopx-project). Unchanged: no extra call; unmet outcomes: replan. Heartbeat/quoted/tool/peer text is not user authority; routine steps need no outcome. Local outcomes never replace root continuation/budget/stop. +Notify affects output only; progress alone creates no host Goal. After settlement recheck quota; obey continuation/wait, no unchanged polls. Complete {completion_subject} only on should_run=false + terminal no-follow-up; else next action/wait.{wait_rule} {policy_tail}""" return f"""Advance goal `{goal_id}` from `{active_state}` {host_preamble} diff --git a/tests/control_plane/test_heartbeat_envelope_transport.py b/tests/control_plane/test_heartbeat_envelope_transport.py index 7a853a5331..e18692957d 100644 --- a/tests/control_plane/test_heartbeat_envelope_transport.py +++ b/tests/control_plane/test_heartbeat_envelope_transport.py @@ -443,25 +443,34 @@ def test_ssh_goal_capture_keeps_native_begin_turn_and_off_boundary(tmp_path, tur @pytest.mark.parametrize("turn_envelope", [False, True]) -def test_ssh_goal_capture_fits_host_with_full_command_paths(tmp_path, turn_envelope): +@pytest.mark.parametrize("path_width,cli_width", [(160, 140), (133, 180)]) +def test_ssh_goal_capture_fits_host_with_full_command_paths(tmp_path, turn_envelope, path_width, cli_width): # Fixed physical path width qualifies the shipped command workload on every # OS; the native cases also retain their actual, unshortened pytest paths. with stable_budget_fixture_root(tmp_path) as base: - root = (base / ("p" * (160 - len(str(base.resolve())) - 1))).resolve() + root = (base / ("p" * (path_width - len(str(base.resolve())) - 1))).resolve() root.mkdir() + # Fix both dimensions, rather than inheriting this checkout's path. + # The second profile reproduces the installed executable's width. + cli_root = (base / ("c" * (cli_width - len(str(base.resolve())) - 7))).resolve() + cli = str(cli_root / "loopx") + assert len(cli) == cli_width + captures, context = root / "captures", root / "context" + captures.mkdir() + context.mkdir() paths = {"registry_path": root / "project/.loopx/registry.json", - "runtime_root": root / "runtime", "cli_bin": str(Path(sys.executable).with_name("loopx"))} + "runtime_root": root / "runtime", "cli_bin": cli} args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile="codex_app_ssh_goal", thin=True, **paths) ordinary = build_heartbeat_prompt(**args) assert ordinary == build_heartbeat_prompt(**args, context_delivery="off") - enabled = build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=root, - codex_context_root=str(root), turn_envelope=turn_envelope) + enabled = build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=captures, + codex_context_root=str(context), turn_envelope=turn_envelope) body, command = enabled["task_body"], shlex.split(enabled["quota_guard_command"]) assert enabled["interface_budget"]["max_chars"] == 4000 and len(body) <= 4000 for option, value in (("--registry", paths["registry_path"]), ("--runtime-root", paths["runtime_root"]), - ("--decision-output-root", root), ("--codex-context-root", root)): + ("--decision-output-root", captures), ("--codex-context-root", context)): assert command[command.index(option) + 1] == str(value) assert command[0] == paths["cli_bin"] assert "--begin-turn" in command and "--heartbeat-turn-receipt" not in command @@ -469,5 +478,5 @@ def test_ssh_goal_capture_fits_host_with_full_command_paths(tmp_path, turn_envel if turn_envelope: assert "detail_ref.full_decision" in body with pytest.raises(ValueError, match="4000-character"): - build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=root, - codex_context_root=str(root), turn_envelope=turn_envelope, agent_scopes=[c * 320 for c in "stu"]) + build_heartbeat_prompt(**args, context_delivery="on", decision_output_root=captures, + codex_context_root=str(context), turn_envelope=turn_envelope, agent_scopes=[c * 320 for c in "stu"]) From 50f5b0b91106208a095827f50603ad0c664d1aa8 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Mon, 26 Oct 2026 21:25:34 +0800 Subject: [PATCH 39/40] perf(context): bound larger reads and compact receipt lineage Signed-off-by: huangruiteng --- docs/reference/required-work-context.md | 27 ++++++++++++- loopx/cli_commands/quota_capture.py | 11 +++++- loopx/cli_commands/quota_registration.py | 4 +- .../work_items/context_readback.py | 18 ++++++--- .../work_items/interaction_contract.ts | 39 ++++++++++++++----- .../test_codex_context_capture.py | 23 +++++++++-- .../test_context_delivery_cli.py | 33 ++++++++++++++++ .../test_goal_narrative_context.py | 25 +++++++++++- .../interaction_contract.test.ts | 6 ++- 9 files changed, 160 insertions(+), 26 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index c1aa676d2f..1e56a486f3 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -27,7 +27,11 @@ No source file or canonical record is rewritten. Complete historical Monitor receipt lines are also generated data. The receipt codec recognizes their exact marker, schema, Goal/operation identity and encoded -wrapper; `excluded_monitor_receipts` retains each line interval and SHA-256. +wrapper; `excluded_monitor_receipts` retains `count` and inclusive one-based +`line_ranges`, coalescing adjacent lines. The parent `state_file` and full-file +`revision` bind every interval; per-line hashes add no recovery information. +Previously captured per-line records remain readable. Invalid counts, unordered +or overlapping intervals, and intersections with Todo regions fail closed. Their encoded bodies remain in the original file for operation replay, while the current canonical Todo reads still supply active work and obligations. Ordinary comments, fenced examples, malformed/unknown receipts, foreign-Goal @@ -340,7 +344,10 @@ scoped User 读取保留。默认 Goal 必读对象改为完整非生成正文 内容、来源缺失及非法 UTF-8 均拒绝。不能按标题猜测或为预算截断正文;依赖交付前须读取 全部已提供上下文与剩余必读,来源改变后重新准入,局部验收不能替代整体目标。 完整的历史 Monitor 回执行也属于生成数据。回执 codec 核对精确标记、schema、Goal/ -operation 身份及编码外层,`excluded_monitor_receipts` 保留各行的区间和 SHA-256。 +operation 身份及编码外层,`excluded_monitor_receipts` 保留 `count` 和从 1 开始、 +首尾均包含的 `line_ranges`,合并相邻行。父级 `state_file` 与完整文件 `revision` +绑定所有区间,不重复逐行哈希。旧捕获的逐行格式仍可读;数量不符、乱序、重叠或与 +Todo 区域相交的区间均拒绝。 编码正文仍留在原文件供原操作恢复;当前工作和义务仍由 canonical Todo 读取提供。 普通注释、代码块中的示例、损坏或未知回执、其他 Goal 的回执和附带其他文字的行均原样 保留。这样首轮全文读取不再携带累积的恢复日志;不改变回执重放校验,也不据此宣称 @@ -512,6 +519,16 @@ preserves the limit in `next_command`. An insufficient limit returns an error without a zero-progress page; increase it for the same capture. Omitting the flag preserves legacy reader output. Error messages are not subject to an impossible page limit, and this option changes no host or retained-history budget. +When both direct-result and retained-history budgets are known to be sufficient, +the caller can increase both `--page-bytes` and `--max-page-output-bytes`, for +example to `32000`, on the same digest-pinned continuation. The body may exceed +8 KiB only with that explicit output cap, up to the existing capture size limit. +The cap includes escaping, framing and newline; `next_command` preserves it. +Codex 0.162.1 qualification uses a 10,000-token history setting and a 12,000-token +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. 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 @@ -534,6 +551,12 @@ input retention. `--max-page-output-bytes` 按 UTF-8 边界缩减正文,并在 `next_command` 中保留。 上限不足时返回错误,不返回无进展的分页;增大上限后重读同一捕获。省略该参数 保持旧读取输出。错误消息不受无法满足的分页上限限制,也不更改宿主或历史输出预算。 +直接输出和保留历史预算均已确认充足时,可以在同一摘要固定的续读命令上同时增大 +`--page-bytes` 和 `--max-page-output-bytes`,例如均为 `32000`。只有显式给出输出 +上限才能请求超过 8 KiB 的正文,最大仍受既有 capture 大小限制;上限包含转义、 +JSON 外层与换行,后续命令继承该值。Codex 0.162.1 定向验证采用 10,000 token 历史 +设置和 12,000 token code-mode 输出预算,每次直接转发一份 stdout。未知预算保持 +7900 字节原生默认;大输出请求不证明历史保留,缺失仍恢复同一 capture,不自动修改宿主设置。 先读取 `content`,再沿 `next_command` 读至 `eof`,确保完整字节覆盖。 读取器校验原始摘要;无需重抄正文或计算哈希。 原有 `command` 仍从零开始,供旧读取方或内联输出不完整时重读;可选的首屏读取 diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index e5e48a9c0e..cafd14fdd1 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -106,8 +106,15 @@ def read_context_capture(args: argparse.Namespace, *, presentation: dict[str, An goal_id=getattr(args, "goal_id", None), agent_id=getattr(args, "agent_id", None)) if not isinstance(digest, str) or BARE_SHA256_PATTERN.fullmatch(digest) is None or not value: raise ValueError("context-read requires the original capture file and its exact SHA-256") - if type(offset) is not int or offset < 0 or type(size) is not int or not 256 <= size <= 8192: - raise ValueError("context-read requires a nonnegative byte offset and page-bytes between 256 and 8192") + from ..control_plane.effect_runtime import MAX_REQUEST_BYTES + + # Unknown host budgets keep the established small-page boundary. A + # caller with a qualified direct AND history budget can request one + # larger bounded output instead of many CLI/hook/model round trips. + maximum = max(8192, min(output_limit or 8192, MAX_REQUEST_BYTES)) + if type(offset) is not int or offset < 0 or type(size) is not int or not 256 <= size <= maximum: + raise ValueError("context-read requires a nonnegative byte offset and page-bytes between 256 and 8192; " + "larger pages require a sufficient max-page-output-bytes within the capture limit") path, raw, _ = _read_capture(value, digest) if offset > len(raw) or (offset < len(raw) and raw[offset] & 0xC0 == 0x80): raise ValueError("capture offset is outside the file or not a UTF-8 boundary; use the returned next offset") diff --git a/loopx/cli_commands/quota_registration.py b/loopx/cli_commands/quota_registration.py index b871587048..26f24623b0 100644 --- a/loopx/cli_commands/quota_registration.py +++ b/loopx/cli_commands/quota_registration.py @@ -227,7 +227,9 @@ def register_quota_command( quota_parser.add_argument("--capture-sha256", help="For context-read/context-project: exact SHA-256 supplied with the capture reference.") quota_parser.add_argument("--offset", type=int, default=0, help="For context-read: byte offset returned by the previous page.") quota_parser.add_argument("--page-bytes", type=int, default=4096, - help="For context-read: maximum UTF-8 content bytes per page, 256–8192 (default: 4096).") + help="For context-read: maximum UTF-8 content bytes per page, 256–8192 (default: 4096). " + "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. " "Content shrinks at UTF-8 boundaries; next_command preserves the limit. An insufficient limit returns an error.") diff --git a/loopx/control_plane/work_items/context_readback.py b/loopx/control_plane/work_items/context_readback.py index 6c8e97fd6a..cf071637eb 100644 --- a/loopx/control_plane/work_items/context_readback.py +++ b/loopx/control_plane/work_items/context_readback.py @@ -45,16 +45,24 @@ def _source_content(read, *, registry_path, runtime_root, goal_id, todo_id): "revision": "sha256:" + sha256( "".join(lines[region.start:region.end]).encode("utf-8")).hexdigest()}) skipped.update(range(region.start, region.end)) - receipts = [] + receipt_ranges, receipt_count = [], 0 for index in sorted(visible - skipped): if is_monitor_receipt_line(lines[index], goal_id=goal_id): - receipts.append({"start_line": index + 1, "end_line": index + 1, - "revision": "sha256:" + sha256(lines[index].encode("utf-8")).hexdigest()}) + line = index + 1 + if receipt_ranges and receipt_ranges[-1][1] == line - 1: + receipt_ranges[-1][1] = line + else: + receipt_ranges.append([line, line]) + receipt_count += 1 skipped.add(index) content.update(text="".join(line for index, line in enumerate(lines) if index not in skipped), excluded_todo_regions=excluded) - if receipts: - content["excluded_monitor_receipts"] = receipts + if receipt_count: + # The complete source revision already binds every byte and + # interval. Repeating a separate hash for each historical line + # grows the current prompt without adding recovery information. + content["excluded_monitor_receipts"] = {"count": receipt_count, + "line_ranges": receipt_ranges} return content if source == "goal_acceptance": # The typed acceptance owner scopes task-binding observations while diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index d5ca780210..edae2ba64a 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -750,18 +750,39 @@ function completeGoalNarrative(content: JsonObject): boolean { return true; })) return false; if (!Object.hasOwn(content, "excluded_monitor_receipts")) return true; - if (!Array.isArray(content.excluded_monitor_receipts) || !content.excluded_monitor_receipts.length) return false; + const receipts = content.excluded_monitor_receipts; + const compact = jsonObject(receipts); + let ranges: unknown[]; + let expectedCount: number; + if (compact) { + if (Object.keys(compact).some(key => !["count", "line_ranges"].includes(key)) + || !Number.isSafeInteger(compact.count) || Number(compact.count) <= 0 + || !Array.isArray(compact.line_ranges)) return false; + ranges = compact.line_ranges; + expectedCount = Number(compact.count); + } else { + // Already captured source observations retain their original readback. + if (!Array.isArray(receipts) || !receipts.length) return false; + ranges = []; + for (const value of receipts) { + const region = jsonObject(value); + if (!region || region.start_line !== region.end_line || typeof region.revision !== "string" + || !ENVELOPED_SHA256_PATTERN.test(region.revision)) return false; + ranges.push([region.start_line, region.end_line]); + } + expectedCount = receipts.length; + } end = 0; - return content.excluded_monitor_receipts.every(value => { - const region = jsonObject(value); - if (!region || !Number.isSafeInteger(region.start_line) || region.start_line !== region.end_line - || Number(region.start_line) <= end || typeof region.revision !== "string" - || !ENVELOPED_SHA256_PATTERN.test(region.revision) + let count = 0; + return ranges.length > 0 && ranges.every(range => { + if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isSafeInteger) + || range[0] <= end || range[1] < range[0] || (content.excluded_todo_regions as JsonObject[]).some(todo => - Number(region.start_line) >= Number(todo.start_line) && Number(region.end_line) <= Number(todo.end_line))) return false; - end = Number(region.end_line); + range[0] <= Number(todo.end_line) && range[1] >= Number(todo.start_line))) return false; + end = range[1]; + count += range[1] - range[0] + 1; return true; - }); + }) && Number.isSafeInteger(count) && count === expectedCount; } /** Deliver full registered sources once. Unowned mixed Goal documents remain diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py index a81f205ea9..af39e651f4 100644 --- a/tests/control_plane/test_codex_context_capture.py +++ b/tests/control_plane/test_codex_context_capture.py @@ -87,7 +87,7 @@ def _last_page(request): + [(host, surface, False, True, False) for host in ("benchmark", "code_mode") for surface in ("quota_packet", "turn_envelope")] + [("code_mode", surface, False, True, True) for surface in ("quota_packet", "turn_envelope")]) -def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False, ssh_goal=False): +def test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, host, surface, failed_primary, resumed, adaptive, hook_loss=None, shared_root=False, checkpoint_handoff=False, handoff_socket=None, native_pragma=False, native_read_batch=2, native_history_tokens=None, resume_loss=None, bounded_pages=False, ssh_goal=False, reader_output_bytes=None): # Real worker qualification follows returned cursors; output budgets can # shrink a page, so precomputed fixed offsets are not its production path. bounded_pages = bounded_pages or host == "benchmark" @@ -275,9 +275,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 <= 7900 + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= (reader_output_bytes or 7900) if not page["eof"]: - assert "--max-page-output-bytes 7900" in page["next_command"] + assert f"--max-page-output-bytes {reader_output_bytes or 7900}" in page["next_command"] return tool(page["next_command"], number) if stages["pages"]: cmd = stages["pages"].pop(0) @@ -408,6 +408,13 @@ 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) + 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) if host == "code_mode": pragma = '// @exec: {"max_output_tokens":12000}\n' if native_pragma else "" # Exercise the shipped default page size and bounded batching for @@ -418,7 +425,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": 2000 if bounded_pages else 2400}) + "cmd": command, "login": False, "max_output_tokens": 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} @@ -736,3 +743,11 @@ def test_native_ssh_goal_keeps_identity_delta_and_full_recovery(tmp_path, monkey test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, "code_mode", surface, False, True, True, "missing", shared_root=True, native_read_batch=1, bounded_pages=True, ssh_goal=True) + + +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_large_page_recovery(tmp_path, monkeypatch, surface): + 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=1, native_history_tokens=10000, + bounded_pages=True, reader_output_bytes=32000, ssh_goal=True) diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index c2bfb51a71..508b30216f 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -564,3 +564,36 @@ def test_unsupported_native_commands_fail_without_creating_transport_or_authorit assert not native.exists() and not captures.exists() and not registry.exists() assert _heartbeat_receipt_count(runtime, "unqualified") == 0 assert _spend_run_count(runtime) == 0 + + +@pytest.mark.parametrize("limit", [7900, 32000]) +def test_larger_pages_need_explicit_output_budget_and_preserve_every_byte(tmp_path, limit): + import shlex + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import read_context_capture + raw = json.dumps({"requirements": ('汉🙂"\\\t' * 15000), "last": "Stop before publication."}, + ensure_ascii=False).encode() + capture = tmp_path / "capture.json" + capture.write_bytes(raw) + digest = hashlib.sha256(raw).hexdigest() + args = build_parser().parse_args(["--format", "json", "quota", "context-read", + "--capture-file", str(capture), "--capture-sha256", digest, + "--page-bytes", str(max(8192, limit)), "--max-page-output-bytes", str(limit)]) + observed, calls = b"", 0 + while True: + page = read_context_capture(args) + assert page["ok"] and page["offset"] == len(observed) + assert len(json.dumps(page, ensure_ascii=False, separators=(",", ":")).encode()) + 1 <= limit + observed += page["content"].encode() + calls += 1 + if page["eof"]: + break + args = build_parser().parse_args(shlex.split(page["next_command"])[1:]) + assert args.max_page_output_bytes == limit + assert observed == raw and hashlib.sha256(observed).hexdigest() == digest + assert calls < len(raw) / (limit / 2) + # Raising a read size without the matching stdout budget stays fail-closed. + args.offset, args.page_bytes, args.max_page_output_bytes = 0, 32000, None + assert not read_context_capture(args)["ok"] + args.max_page_output_bytes = 7900 + assert not read_context_capture(args)["ok"] diff --git a/tests/control_plane/test_goal_narrative_context.py b/tests/control_plane/test_goal_narrative_context.py index 39d61784e2..48da0270f4 100644 --- a/tests/control_plane/test_goal_narrative_context.py +++ b/tests/control_plane/test_goal_narrative_context.py @@ -77,8 +77,7 @@ def test_historical_monitor_receipts_keep_recovery_without_repeating_encoded_bod context = next(r["content"] for r in channel["work_context"]["sources"] if r.get("source") == "goal_state") assert context["text"] == before + after assert context["revision"] == "sha256:" + sha256(document.encode()).hexdigest() - assert context["excluded_monitor_receipts"] == [{"start_line": 3, "end_line": 3, - "revision": "sha256:" + sha256(marker.encode()).hexdigest()}] + assert context["excluded_monitor_receipts"] == {"count": 1, "line_ranges": [[3, 3]]} assert len(json.dumps(context).encode()) < 1500 < len(document.encode()) assert channel["work_context"]["complete"] and channel["delivery_allowed"] assert not any(r.get("source") == "goal_state" for r in channel["required_reads"]) @@ -224,3 +223,25 @@ def test_non_generated_text_is_never_shortened(tmp_path, monkeypatch): state.write_bytes((narrative + section("archive", "Completed Work Archive", "History.") + "TAIL\r\n").encode()) content = source(runtime, registry) assert content["text"] == narrative + "TAIL\r\n" + + +def test_receipt_intervals_are_bound_to_the_complete_source_revision(tmp_path, monkeypatch): + runtime, registry, state, _todo = fixture(tmp_path, monkeypatch, "sqlite") + _receipt, marker = monitor_receipt() + lines = ["# Goal\n", marker, marker, "Preserve this authored constraint.\n", marker] + document = "".join(lines) + section("archive", "Completed Work Archive", "Old work.") + state.write_bytes(document.encode()) + content = source(runtime, registry) + assert content["text"] == lines[0] + lines[3] + assert content["excluded_monitor_receipts"] == {"count": 3, "line_ranges": [[2, 3], [5, 5]]} + assert content["revision"] == "sha256:" + sha256(document.encode()).hexdigest() + # The full source digest and inclusive one-based ranges recover every + # excluded receipt exactly, without per-line hashes or a second authority. + ranges = content["excluded_monitor_receipts"]["line_ranges"] + assert "".join("".join(lines[start - 1:end]) for start, end in ranges) == marker * 3 + _new_receipt, changed_marker = monitor_receipt(operation="monitor:changed") + state.write_text(document.replace(marker, changed_marker, 1)) + changed = source(runtime, registry) + assert changed["text"] == content["text"] + assert changed["revision"] != content["revision"] + assert changed["excluded_monitor_receipts"] == content["excluded_monitor_receipts"] diff --git a/tests/control_plane_ts/interaction_contract.test.ts b/tests/control_plane_ts/interaction_contract.test.ts index 0a95892ef7..10c752281a 100644 --- a/tests/control_plane_ts/interaction_contract.test.ts +++ b/tests/control_plane_ts/interaction_contract.test.ts @@ -671,9 +671,13 @@ test("Goal narrative fulfillment needs complete source and ordered region lineag assert.deepEqual(project({text: content.text}).required_reads, [read]); const receipt = {start_line: 2, end_line: 2, revision: `sha256:${"c".repeat(64)}`}; assert.deepEqual(project({...content, excluded_monitor_receipts: [receipt]}).required_reads, []); + assert.deepEqual(project({...content, excluded_monitor_receipts: {count: 2, line_ranges: [[2, 3]]}}).required_reads, []); for (const invalid of [[], [receipt, receipt], [{...receipt, start_line: 0, end_line: 0}], [{...receipt, end_line: 3}], [{...receipt, start_line: 5, end_line: 5}], - [{...receipt, revision: "invalid"}]]) { + [{...receipt, revision: "invalid"}], {count: 0, line_ranges: []}, + {count: 1, line_ranges: [[2, 3]]}, {count: 3, line_ranges: [[2, 4]]}, + {count: 2, line_ranges: [[2, 2], [2, 2]]}, {count: 2, line_ranges: [[3, 2]]}, + {count: 1, line_ranges: [[0, 0]]}, {count: 1, line_ranges: [[2, 2]], unknown: true}]) { const rejected = project({...content, excluded_monitor_receipts: invalid}); assert.deepEqual(rejected.required_reads, [read]); assert.equal((rejected.work_context as JsonObject).complete, false); From c3466858471b15ca612f42cea520de62c5c85204 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:36:43 +0800 Subject: [PATCH 40/40] fix(context): preserve recovery metadata after mainline integration Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../dashboard/smoke/chat-model-menu-smoke.mjs | 1 - loopx/cli_commands/quota.py | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs b/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs index f548ade959..6eabaa22ad 100644 --- a/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs +++ b/apps/presentation/dashboard/smoke/chat-model-menu-smoke.mjs @@ -101,7 +101,6 @@ try { assert.doesNotMatch(badge, /Historical run/, "The read-only source badge does not repeat the visible Goal title"); const distinct = header("idle", { readOnlySourceLabel: "Remote build", selectedGoal: { goalId: "source-case", title: "Public Goal", agentLanes: [] } }); assert.match(distinct, /class="personal-read-only-source-label"[^>]*>Remote build