From a18781977b340bce2dc82f8d17ca90e9a67495ee Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:42:05 +0800 Subject: [PATCH] docs(context): keep native page readers in the foreground Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/heartbeat-automation-prompt.md | 5 ++ docs/reference/required-work-context.md | 13 +++ .../test_codex_context_boundary.py | 83 +++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 87750719b0..491315af69 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -486,6 +486,11 @@ reader budget declaration. The initial guard stays small. Its instruction reques 22000 tokens per reader result and 24000 for code-mode emission, with one directly forwarded stdout per call. Unconfigured hosts retain the small-page default; the isolated benchmark worker applies this profile automatically when opted in. +Keep short context readers in the foreground (`exec_command`'s normal 10000 ms +wait). Early output followed by polling can leave the native post-tool hook with +only an empty tail, so a visibly complete page still cannot establish a baseline. +Consume every chunk if a command yields and preserve full fallback when hook +coverage is incomplete; do not re-admit or invent a retention receipt. Preserve other configuration. Review and trust only these exact hooks through Codex's native hook trust surface; the CLI does not install files, approve hashes, enable diagnostics or run a guard. diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index cf07e55b8a..c557d7442d 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -273,6 +273,14 @@ 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. +Keep these short local reader commands in the foreground; for `exec_command`, +use its normal 10000 ms wait rather than an aggressive early yield. A native +host can deliver a page before the process exits but give `PostToolUse` only the +remaining output when polling finishes. Complete model-visible text then does +not imply complete hook coverage. If a reader yields, consume all its output +chunks; missing hook coverage keeps the next decision on full delivery. Do not +fabricate a read receipt or rerun admission to make it differential. Continuing +that captured decision after context loss still uses its exact full recovery. 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 @@ -508,6 +516,11 @@ SSH Goal 使用 `--runtime-profile codex_app_ssh_goal` 生成 context-config 和 后续命令与换行)限制为 7900 字节。先消费内联页,再跟随 next_command;保留历史 上限未知时,每次只读一个有界输出。该上限不证明模型保留。Code Mode 直接转发 exec stdout;把整包重打进哈希工具不构成原始字节已被保留的证明。 +这些短时本地读取命令应前台等到结束;`exec_command` 使用正常的 10000 ms 等待, +避免激进提前返回。原生宿主可能先把页面交给模型,却只把轮询结束时剩余的输出交给 +`PostToolUse`;模型可见正文完整不等于 hook 观察范围完整。读取器若提前返回,仍须 +消费全部输出片段;hook 覆盖缺失时,下次决定继续全量投递。不伪造读取回执或重跑 +准入来制造差分;丢失上下文后继续该已捕获决定,仍沿它的精确全量恢复引用读取。 原生配置同时返回 `config_overrides`:应用后,续读输出上限为 64000 字节,原生历史 上限为 20000 token,首次 guard 仍保持 7900 字节。开启的 benchmark worker 在其 隔离会话内自动应用;持久指引要求每次续读直接输出预算 22000 token、Code Mode diff --git a/tests/control_plane/test_codex_context_boundary.py b/tests/control_plane/test_codex_context_boundary.py index fdd3ea48f6..328b2fa6b4 100644 --- a/tests/control_plane/test_codex_context_boundary.py +++ b/tests/control_plane/test_codex_context_boundary.py @@ -8,6 +8,8 @@ import json import os +from pathlib import Path +import re import shlex import subprocess import sys @@ -146,6 +148,87 @@ def _configure_provider(home, url): 'wire_api="responses"\nrequires_openai_auth=false\n') +@pytest.mark.parametrize("early_yield", [False, True]) +def test_native_reader_requires_complete_hook_output_even_when_model_saw_page(tmp_path, early_yield): + """A completed process cannot turn an unobserved early page into a baseline. + + The gate, rather than elapsed time, keeps the yielding reader alive. Future + hosts may deliver accumulated output to the hook; accept that only when its + complete page is actually observed. Inference remains entirely local. + """ + from loopx.control_plane.turn_driver import codex_context as context + + binary, env, workspace, home = _native_fixture(tmp_path) + env["PYTHONPATH"] = str(Path(__file__).resolve().parents[2]) + root, capture = workspace / "context", workspace / "capture" + root.mkdir(mode=0o700) + capture.mkdir(mode=0o700) + gate, view_file, observations = workspace / "release", workspace / "view.json", home / "observed.jsonl" + hook = home / "observe.py" + hook.write_text( + "import json, pathlib, sys\n" + "from loopx.control_plane.turn_driver.codex_context import codex_context_hook\n" + "event=json.load(sys.stdin)\n" + f"result=codex_context_hook({str(root)!r},event)\n" + f"with pathlib.Path({str(observations)!r}).open('a') as f: f.write(json.dumps(event)+'\\n')\n" + "print(json.dumps(result))\n") + handler = {"type": "command", "command": shlex.join([sys.executable, str(hook)]), "timeout": 10} + (home / "hooks.json").write_text(json.dumps({"hooks": { + name: [{"hooks": [handler]}] for name in ("SessionStart", "PostToolUse")}})) + publish = workspace / "publish.py" + publish.write_text( + "import json, pathlib, time\n" + "from loopx.control_plane.turn_driver.codex_context import project_codex_context\n" + "from loopx.cli_commands.quota_capture import native_context_capture_view\n" + f"capture=pathlib.Path({str(capture)!r})\n" + "body={'complete':True,'sources':[{'source':'selected_todo','content':{'todo':" + "{'todo_id':'todo_fixture','text':'Preserve the original requirement. '*40,'status':'open'}}}]}\n" + f"full=project_codex_context(body,directory={str(root)!r},capture_directory=capture," + "goal_id='fixture',agent_id='agent',surface='work_context',capture_id='fixture-capture')\n" + f"view=native_context_capture_view(full,capture,context_directory={str(root)!r})\n" + "text=json.dumps(view,separators=(',',':'))\n" + f"pathlib.Path({str(view_file)!r}).write_text(text)\n" + "print(text,flush=True)\n" + + (f"while not pathlib.Path({str(gate)!r}).exists(): time.sleep(0.01)\n" if early_yield else "")) + + def response(request, number, message): + if number == 1: + name, args = "exec_command", {"cmd": shlex.join([sys.executable, str(publish)]), + "yield_time_ms": 1000 if early_yield else 10000, "max_output_tokens": 5000} + elif early_yield and number == 2: + match = re.search(r"Process running with session ID (\d+)", _input_text(request)) + assert match, "The native reader must yield before the fixture releases its gate" + gate.write_text("release") + name, args = "write_stdin", {"session_id": int(match[1]), "chars": "", + "yield_time_ms": 10000, "max_output_tokens": 5000} + else: + return message + return {"type": "function_call", "id": f"fc_{number}", "call_id": f"call_{number}", + "name": name, "arguments": json.dumps(args)} + + try: + with local_inference_provider(response) as (url, _, requests): + _configure_provider(home, url) + result = subprocess.run([binary, "exec", "--dangerously-bypass-hook-trust", "--skip-git-repo-check", + "--sandbox", "danger-full-access", "--json", "-C", str(workspace), "Synthetic reader fixture."], + env=env, capture_output=True, text=True, timeout=40) + finally: + gate.write_text("release") + assert result.returncode == 0, result.stderr + view = json.loads(view_file.read_text()) + assert view["offset"] == 0 and view["eof"] is True + assert view_file.read_text() in _input_text(requests[1]) # actual complete model-input page + events = [json.loads(line) for line in observations.read_text().splitlines()] + observed = [page for event in events if event["hook_event_name"] == "PostToolUse" + for page in context._context_pages(event.get("tool_response"))] + state = json.loads((root / "session.json").read_text()) + complete_hook = any(page == view for page in observed) + assert bool(state.get("candidate")) is complete_hook + assert bool(state.get("pending")) is not complete_hook + if not early_yield: + assert complete_hook + + @pytest.mark.parametrize("source", ["inline", "file"]) def test_native_late_hook_discovery_does_not_deliver_session_context(tmp_path, source): """Discovery/trust after start is not SessionStart delivery to that thread.