From 098f4d49d6c552214515289dd7dd50ed40e6a17a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:50:28 +0800 Subject: [PATCH] fix(context): include nested output budgets in recovery hooks Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 13 ++++++++++++- loopx/control_plane/turn_driver/codex_context.py | 12 ++++++++++-- tests/control_plane/test_codex_context_lifecycle.py | 2 ++ tests/control_plane/test_codex_context_resume.py | 11 ++++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 8314169b51..4f4534a256 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -435,7 +435,14 @@ output profile. Both the initial guard and continuations use that page budget, including when the shell lacks the declaration. The worker applies this configuration automatically inside its isolated opt-in trial. The persistent instruction requests 22000 tokens for each context-read tool result and 24000 for its code-mode emission, -forwarding one stdout at a time. Remove the generated output overrides together with the +forwarding one stdout at a time. The post-compaction/recovery hook carries the same +per-call budget guidance so recovery does not depend on retaining the setup text. +Both the nested reader and outer code-mode emission can truncate independently; +per-call requests do not prove retained history or change host configuration. +If either layer truncates, retry the affected range of the same capture with +smaller content and complete-output byte budgets. This avoids a transport retry +failure; it does not make an oversized total context fit or qualify scoped reads. +Remove the generated output overrides together with the context setup on rollback; saving a machine preference alone does not apply them. This output sizing does not prove retained input, lower fees or task benefit. For an SSH Goal, replace `--codex-app` @@ -677,6 +684,10 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的 开启的 benchmark worker 在其 隔离会话内自动应用;持久指引要求每次续读直接输出预算 22000 token、Code Mode 输出预算 24000 token,每次直接转发一份 stdout。 +压缩/恢复 hook 同样携带这段调用预算指引,避免恢复依赖已经丢失的配置正文。 +内层读取与外层 Code Mode 输出可分别截断;单次预算请求不证明历史保留,也不修改宿主配置。 +任一层截断时,减小正文和完整输出字节预算,重读同一 capture 的受影响范围。 +这仅修复传输重试故障,不代表超大的完整上下文已能容纳,也不证明范围裁剪合格。 回滚时同时移除生成的输出覆盖项;仅保存机器偏好并不会应用这些宿主配置。 该设置不证明输入保留、总费用降低或任务效果改善。 读取器校验不可变 capture 的摘要。Post-tool 适配层复用显式输入观察的字节范围校验, diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index a4f96474f5..fe2ee772a2 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -225,6 +225,13 @@ def _context_continuation_instructions(command: str) -> str: ) +# Both persistent setup and post-compaction recovery must describe the nested +# output budgets. The hook may be the only instruction retained after loss. +_NATIVE_CONTEXT_OUTPUT_GUIDANCE = ( + "For 64000-byte pages request reader max_output_tokens=22000 and code-mode exec pragma max_output_tokens=24000. " +) + + def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_id: str, cli_argv: list[str], session_root: bool = False, page_output_bytes: int = NATIVE_CONTEXT_PAGE_OUTPUT_BYTES) -> str: @@ -239,8 +246,8 @@ def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_i _context_continuation_instructions(command) + "When recovering a required full read, follow next_command through eof and verify complete bytes/digest. " + ("The generated native configuration supports 64000-byte context-read outputs and a 20000-token history limit. " - "Read one continuation per tool call with max_output_tokens=22000. In code-mode, also set the exec " - "pragma max_output_tokens to 24000 and forward successful stdout directly with text(result.output). " + + _NATIVE_CONTEXT_OUTPUT_GUIDANCE + + "Read one continuation per tool call; in code-mode, forward successful stdout directly with text(result.output). " if page_output_bytes == NATIVE_CONTEXT_LARGE_OUTPUT_BYTES else "") + "For code-mode reads, size batches against both direct-result and retained-history output limits; " "max_output_tokens alone does not establish the latter. Read one output at a time with " @@ -342,6 +349,7 @@ def codex_context_hook(directory: str | None, event: dict[str, Any]) -> dict[str return {"hookSpecificOutput": {"hookEventName": name, "additionalContext": "LoopX context baseline invalidated. " + _context_continuation_instructions(recovery["command"]) + + _NATIVE_CONTEXT_OUTPUT_GUIDANCE + "When completing a required full-capture read: " + 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 5b11258d42..37a8bb3fd3 100644 --- a/tests/control_plane/test_codex_context_lifecycle.py +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -76,6 +76,7 @@ def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_p "shell_environment_policy.set.LOOPX_CONTEXT_PAGE_OUTPUT_BYTES": "64000"} assert "--max-page-output-bytes 64000" in native["developer_instructions"] assert "max_output_tokens=22000" in native["developer_instructions"] + assert "max_output_tokens=24000" in native["developer_instructions"] assert "hooks.state" not in native and list(tmp_path.iterdir()) == [] missing_identity = subprocess.run(command[:-2], cwd=tmp_path, env={**os.environ, "PYTHONPATH": str(repo)}, capture_output=True, text=True, timeout=15) @@ -119,6 +120,7 @@ def test_lifecycle_invalidates_and_recovers_current_capture_without_bodies(tmp_p assert first["recovery"]["sha256"] in body and "quota context-read" in body # The additional work-Turn/read-obligation distinction is intentional; # this synthetic allowance is not a host output or context limit. + assert "max_output_tokens=22000" in body and "max_output_tokens=24000" in body assert payload["requirements"] not in body and len(body) < 2300 else: assert result == {} diff --git a/tests/control_plane/test_codex_context_resume.py b/tests/control_plane/test_codex_context_resume.py index 82267c2429..ae2a1c4ea7 100644 --- a/tests/control_plane/test_codex_context_resume.py +++ b/tests/control_plane/test_codex_context_resume.py @@ -105,6 +105,10 @@ def test_compact_full_recovery_becomes_next_resume_baseline(tmp_path, monkeypatc assert invalid["context_epoch"] != before["context_epoch"] assert invalid["recovery"] == before["recovery"] assert invalid["recovery"]["command"] in str(recovery_instruction) + guidance = recovery_instruction["hookSpecificOutput"]["additionalContext"] + assert "max_output_tokens=22000" in guidance + assert "max_output_tokens=24000" in guidance + assert "grants no new authority/freshness" in guidance and "retention proof" in guidance reference = invalid["recovery"] offset, index = 0, 0 @@ -254,7 +258,12 @@ def hook(values): assert "candidate" not in after and "pending" not in after assert after["context_epoch"] != before["context_epoch"] assert after["recovery"] == reference - assert reference["command"] in result["hookSpecificOutput"]["additionalContext"] + guidance = result["hookSpecificOutput"]["additionalContext"] + assert reference["command"] in guidance + # Actual CLI hook recovery must stand alone after persistent context loss. + assert "max_output_tokens=22000" in guidance + assert "max_output_tokens=24000" in guidance + assert "grants no new authority/freshness" in guidance and "retention proof" in guidance _, fallback = project(tmp_path, "after-fifo", payload=payload) assert fallback["mode"] == "full" and fallback["context"] == payload assert Path(reference["path"]).read_bytes() == captured