Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/heartbeat-automation-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions docs/reference/required-work-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions tests/control_plane/test_codex_context_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
Expand Down Expand Up @@ -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.
Expand Down
Loading