diff --git a/benchmark/edgebench/run.py b/benchmark/edgebench/run.py index 3d4c3f3e86..eb692b9d19 100644 --- a/benchmark/edgebench/run.py +++ b/benchmark/edgebench/run.py @@ -126,6 +126,8 @@ def main(argv=None): help="New-run default: native public feedback; blind/best-only are explicit controls") parser.add_argument("--turn-envelope", action="store_true", help="Opt-in short heartbeat context with same-invocation full captures") + parser.add_argument("--context-delivery", choices=("off", "on"), default="off", + help="Experimental native Codex benchmark heartbeat reuse only; off by default") parser.add_argument("--native-goal-continue", action="store_true", help="Opt-in: when a native Goal declares completion with budget left, start " "continuation Goals until the budget edge (native-goal worker only)") @@ -190,6 +192,8 @@ def main(argv=None): parser.error("--task-entry loopx-planned requires a heartbeat worker") if args.turn_envelope and args.worker not in {"heartbeat-resume", "heartbeat-explore"}: parser.error("--turn-envelope requires a heartbeat worker") + if args.context_delivery != "off" and not args.worker.startswith("heartbeat-"): + parser.error("--context-delivery requires a heartbeat worker") if args.native_goal_continue and args.worker != "native-goal": parser.error("--native-goal-continue requires --worker native-goal") if (args.replan_after_turns is not None or args.replan_after_todos is not None @@ -320,6 +324,7 @@ def main(argv=None): task_objective=task_objective, task_entry=args.task_entry, turn_envelope=args.turn_envelope, + context_delivery=args.context_delivery, native_goal_continue=args.native_goal_continue, replan_after_turns=args.replan_after_turns, replan_after_todos=args.replan_after_todos, @@ -383,6 +388,7 @@ def main(argv=None): "loopx_commit": pins[0], "runner_commit": pins[1], "sforge_source": active_source_receipt(), **turn_envelope_receipt(explicit=args.turn_envelope), + **({"context_delivery": "on"} if args.context_delivery != "off" else {}), **({"native_goal_continue": True} if args.native_goal_continue else {}), "task_sha256": hashlib.sha256(task_file.read_bytes()).hexdigest(), "feedback": args.feedback, "internet": task.internet, diff --git a/benchmark/runtime/RUNTIME.md b/benchmark/runtime/RUNTIME.md index 878a49ccf8..2680162dbc 100644 --- a/benchmark/runtime/RUNTIME.md +++ b/benchmark/runtime/RUNTIME.md @@ -45,6 +45,86 @@ fail before execution. Set it back to `false` for the next run to roll back; do not change a running trial's treatment. Captures contain private Goal context. Transport tests do not establish lower token cost or better model decisions. +For an explicitly selected native context-delivery treatment, add +`context_delivery: "on"` to the same heartbeat agent `kwargs` (default `"off"`). +Only the existing Codex heartbeat worker accepts `"on"`; booleans, other modes and +scenario names are rejected. This is an experimental transport setting, separate +from `turn_envelope`. +The existing EdgeBench/SForge entry exposes the same selection through +`--context-delivery on` on a heartbeat worker (`off` is the default). +The worker preserves its existing `generic_cli` scheduler profile. Its execution +owner requires `mode=heartbeat` and explicitly propagates `LOOPX_EXECUTION_MODE=heartbeat` +and `LOOPX_CONTEXT_DELIVERY=on` into the native tool environment. Generic CLI +without these worker declarations stays full. These are caller declarations, +not host authentication or a new admission grant. +It works with ordinary quota or the explicit TurnEnvelope view. The existing +worker stages two product lifecycle hooks in the trial's private Codex home and +queries native `hooks/list` without invoking a model. Only those exact native +hashes receive per-process trust; workspace/plugin hooks are not trusted and no +bypass flag or operator settings are used. Private native trace-v1 diagnostics +are enabled only in this opted-in trial. Install and wake receipts record the +selection; `context_transport.available` records setup, **not** warm eligibility +or delivery/benefit. Unsupported native setup continues ordinary full output. + +The product heartbeat renderer carries `--codex-context-dir` and the original +capture root into guard/reentry commands. The product observer proves intact +baseline content in the current model input before warm reuse; uncertainty +selects full. Startup/fork/compaction reset the epoch. Same-session resume clears +the active call and keeps only an unconfirmed candidate; reuse requires the new +call's complete actual input, never a saved session or file. Each worker wake +uses a separate private trace directory, avoiding repeated scans of old wakes. +The small recovery hook reads the original full capture after compaction, with +no replacement admission or debit. Native worker and app-server code-mode tests +cover resumed warm reuse for both packet views. The opted-in worker also carries +a compact native developer instruction that survives compaction independently +of hook output. If context was lost while continuing a captured decision, +`quota context-read --codex-context-dir DIR --goal-id GOAL --agent-id AGENT` +checks the current native session/workspace and selects the original capture. +Follow its digest-pinned next commands; this read never reruns admission or +spends quota. Missing/foreign references refuse lookup. Native tests omit or +kill all subsequent hooks and still recover every byte through this route. +Installed App adoption remains separate; deterministic transport tests are not autonomous +solver outcomes. Include this persistent instruction in subsequent cost comparisons; +earlier input replays did not include it. Count complete input, recovery reads, native discovery and trace +IO in treatment cost, not just the delta bytes. + +Codex App setup uses the same product configuration renderer as the worker: +`loopx --format json quota context-config --context-delivery on --codex-app --codex-context-root DIR --goal-id GOAL --agent-id AGENT`. +It returns native hooks and persistent recovery instructions, without installing +files or changing trust. A shared root isolates journals by native session; +the trial-local single-session `--codex-context-dir` remains supported. +See [heartbeat setup](../../docs/heartbeat-automation-prompt.md#opt-in-native-context-delivery) +for activation, diagnostics, readback and rollback. Generated setup and a native +app-server test do not by themselves prove installed desktop adoption. + +A simultaneous `fresh_replan` checkpoint listener retains ownership of the same +original capture. It validates and holds the original full decision before any +context projection, then releases its exact digest. Only afterward may the +native observer project full/delta for an uninterrupted tool. A refused/lost +release remains the original typed transport failure, without publishing an +optimized result or rerunning the guard. An interrupted guard continues with +the original full packet; no delta is required for checkpoint recovery. Native +cursor verification also binds the context-directory/root option. The existing +Darwin native-checkpoint refusal remains in force. + +The host-side checkpoint preflight still captures the complete original decision. +With context delivery enabled, its prompt now supplies `quota context-project` +with that original path/hash, Goal/Agent/Turn and private cache location. The +native tool presents the saved decision using the same current-input observer; +no guard, checkpoint, admission or debit is repeated. Cold delivery supplies the +full body and its target; qualified warm delivery supplies the exact delta. +The small response points to bounded reads and an original full-read fallback. +No native setup, invalid cache or uncertain retention keeps full input. A bad +source hash/scope is refused before publication. This does not renew decision +freshness or establish autonomous solver benefit. Default-off preloading is +unchanged. Include the projection command, descriptor, recovery and page reads +in subsequent total-cost comparisons. +Across distinct worker wakes, settle the prior Turn before selecting new work; +reading an old prompt from resumed history cannot supply the next wake's guard. +Do not modify an active/frozen trial. Set `context_delivery: "off"` for a new trial +to roll back; default runs install no hooks, trust or trace settings. No scorer, +budget, task semantics or launch authority changes with this option. + Checkpoint capture is independent of this display experiment. A `fresh_replan` declaration may establish a private capture/handoff socket while `turn_envelope` remains false: the worker keeps the ordinary dispatcher and quota diff --git a/benchmark/runtime/checkpoint.py b/benchmark/runtime/checkpoint.py index 4fa3a93299..dc6c796ea2 100644 --- a/benchmark/runtime/checkpoint.py +++ b/benchmark/runtime/checkpoint.py @@ -153,8 +153,14 @@ def capture_before_replan(env: dict[str, str], receipt: dict[str, Any], body: st preflight_seen = guard_path.exists() guard_path.write_text(guard_run.stdout, encoding="utf-8") guard_path.chmod(0o600) - preloaded_body = (f"Host checkpoint preflight has already executed this wake's original quota guard once. " - f"Read the complete packet at {guard_path} before work, then follow its interaction_contract. " + reading = f"Read the complete packet at {guard_path} before work, then follow its interaction_contract. " + if env.get("LOOPX_CONTEXT_DELIVERY") == "on" and env.get("LOOPX_CODEX_CONTEXT_DIR"): + from .context_delivery import preflight_context_instruction + + reading = preflight_context_instruction(env, guard_path, + hashlib.sha256(guard_path.read_bytes()).hexdigest(), receipt["turn_id"]) + preloaded_body = ("Host checkpoint preflight has already executed this wake's original quota guard once. " + + reading + "Do not repeat the initial guard below; use its selection or recovery command when required.\n\n" + body) receipt["checkpoint"] = {"captured": False, "research_point_qualified": False, "phase": "before_replan"} try: diff --git a/benchmark/runtime/checkpoint_wake.py b/benchmark/runtime/checkpoint_wake.py index 966d8913f4..e0dc0ee2be 100644 --- a/benchmark/runtime/checkpoint_wake.py +++ b/benchmark/runtime/checkpoint_wake.py @@ -136,6 +136,9 @@ def _capture(self, held, deadline): "turn_envelope": self.receipt.get("turn_envelope", False), "capture_root": str(self.capture_root), "socket_path": self.socket_path, "decision_sha256": held["sha256"]} + if self.env.get("LOOPX_CODEX_CONTEXT_DIR"): + guard["codex_context_dir"] = self.env["LOOPX_CODEX_CONTEXT_DIR"] + guard["context_delivery"] = self.env.get("LOOPX_CONTEXT_DELIVERY", "off") reader = readers.enter_context(native_checkpoint_reader(self.env, timeout=max(.1, window - time.time()))) native = native_session_checkpoint(self.env, self.receipt["session"]["session_id"], timeout=max(.1, window - time.time()), interrupted_guard=guard, transport=reader, diff --git a/benchmark/runtime/codex.py b/benchmark/runtime/codex.py index 66d670d3f3..3d60db2ed8 100644 --- a/benchmark/runtime/codex.py +++ b/benchmark/runtime/codex.py @@ -28,8 +28,11 @@ class Execution: validation_command: tuple[str, ...] = () task_entry: str | None = None turn_envelope: bool = False + context_delivery: str = "off" def __post_init__(self) -> None: + if self.context_delivery not in ("off", "on") or (self.context_delivery != "off" and self.mode != "heartbeat"): + raise ValueError("context_delivery supports only off or on with heartbeat execution") if not isinstance(self.turn_envelope, bool) or (self.turn_envelope and self.mode != "heartbeat"): raise ValueError("turn_envelope requires heartbeat execution and a boolean opt-in") if self.mode not in MODES or self.context not in CONTEXTS: @@ -182,6 +185,7 @@ def prepare_codex_home( # policy explicitly so model tools cannot silently lose it. "[shell_environment_policy.set]", 'LOOPX_USAGE_PING = "0"', + *(['LOOPX_CONTEXT_DELIVERY = "on"', 'LOOPX_EXECUTION_MODE = "heartbeat"'] if execution.context_delivery != "off" else []), "[features]", f"goals = {str(execution.native_goal).lower()}", "unified_exec = true", diff --git a/benchmark/runtime/context_delivery.py b/benchmark/runtime/context_delivery.py new file mode 100644 index 0000000000..bf0fe1ce63 --- /dev/null +++ b/benchmark/runtime/context_delivery.py @@ -0,0 +1,98 @@ +"""Explicit trial-local consumer of the product's native context transport. + +No packet semantics live here. The existing worker stages exact product hooks +and trusts only their native hashes; quota owns projection and full recovery. +""" +from __future__ import annotations + +import json +import os +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. + """ + from loopx.command_invocation import loopx_cli_argv + + prefix = [*loopx_cli_argv(env["LOOPX_CLI"]), "--format", "json", "quota"] + source = ["--capture-file", str(path), "--capture-sha256", digest] + project = [*prefix, "context-project", *source, + "--goal-id", env["LOOPX_GOAL_ID"], "--agent-id", env["LOOPX_AGENT_ID"], + "--turn-instance-id", turn_id, "--context-delivery", "on", "--runtime-profile", "generic_cli", "--codex-context-dir", env["LOOPX_CODEX_CONTEXT_DIR"], + "--decision-output-root", str(path.parent / "decisions")] + full = [*prefix, "context-read", *source] + return ("Present that same immutable packet with:\n```sh\n" + shlex.join(project) + + "\n```\nFollow the returned command and 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, " + "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. ") + + +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_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) + 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"]]) + content = json.dumps({"hooks": configuration["hooks"]}) + path = home / "hooks.json" + if path.is_symlink() or (path.exists() and path.read_text() != content): + raise ValueError("native context hooks changed within a trial; use a new trial") + if not path.exists(): + path.write_text(content) + path.chmod(0o600) + # The native host is the hash/key owner. Do not duplicate its hashing or + # bypass trust for unrelated workspace hooks. Discovery launches no model. + with (home / "context-hooks-stderr.log").open("a") as stderr: + with StdioNativeGoalTransport.spawn( + # This process only reads native hook identities. Catalog/plugin + # startup can clone repositories into an otherwise private home; + # it is unrelated to discovery and must not affect solver config. + [env["CODEX_BIN"], "-c", "features.plugins=false", + "-c", "features.remote_plugin=false", "-c", "features.plugin_sharing=false", + "app-server", "--listen", "stdio://"], + cwd=env["LOOPX_PROJECT"], env=env, stderr=stderr, response_timeout_sec=10, + ) as native: + native.request("initialize", {"clientInfo": {"name": "loopx_context_transport", "version": "0.1.0"}, + "capabilities": {"experimentalApi": True}}) + native.notify("initialized", {}) + 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: + raise ValueError("native host did not discover the exact trial context hooks") + entries = [] + for hook in hooks: + key, digest = hook.get("key"), hook.get("currentHash") + if not isinstance(key, str) or not key or not isinstance(digest, str) or not digest: + raise ValueError("native host returned no context-hook trust identity") + entries.append(f"{json.dumps(key)} = {{ trusted_hash = {json.dumps(digest)} }}") + # CLI override keys are dot-separated; native keys contain paths/dots. + # 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"])]) diff --git a/benchmark/runtime/harbor.py b/benchmark/runtime/harbor.py index 4d35d0f0a9..568c0c2dfa 100644 --- a/benchmark/runtime/harbor.py +++ b/benchmark/runtime/harbor.py @@ -72,6 +72,7 @@ def __init__( task_entry=None, planning_timeout_sec=None, turn_envelope=False, + context_delivery="off", native_goal_continue=False, task_objective=None, execution_observation=None, @@ -96,6 +97,7 @@ def __init__( validation_command if validation_command is not None else (), task_entry, turn_envelope, + context_delivery, ) if planning_timeout_sec is not None: raise ValueError( @@ -167,7 +169,8 @@ def __init__( super().__init__(*args, **kwargs) def _turn_envelope_receipt(self) -> dict: - return turn_envelope_receipt(explicit=self.execution.turn_envelope) + return {**turn_envelope_receipt(explicit=self.execution.turn_envelope), + **({"context_delivery": "on"} if self.execution.context_delivery != "off" else {})} def _planning_receipt(self) -> dict: return {"native_goal_continue": True} if self.native_goal_continue else {} @@ -611,6 +614,7 @@ def _worker_env(self, *, cwd: str) -> dict[str, str]: **({"LOOPX_CHECKPOINT_REQUEST": self.checkpoint_request_path} if self.checkpoint_request_path is not None else {}), **({"LOOPX_TURN_ENVELOPE": "1"} if self.execution.turn_envelope else {}), + **({"LOOPX_CONTEXT_DELIVERY": "on"} if self.execution.context_delivery != "off" else {}), **({"LOOPX_NATIVE_GOAL_CONTINUE": "1"} if self.native_goal_continue else {}), "LOOPX_ITERATION_CONTEXT": self.execution.context, "LOOPX_CODEX_SANDBOX": self.execution.sandbox, diff --git a/benchmark/runtime/sforge.py b/benchmark/runtime/sforge.py index 12e3b0f795..62c9d2a943 100644 --- a/benchmark/runtime/sforge.py +++ b/benchmark/runtime/sforge.py @@ -92,6 +92,7 @@ def __init__(self, config, *, profile: str, cwd: str, task_objective: str | None = None, task_entry: str | None = None, turn_envelope: bool = False, + context_delivery: str = "off", native_goal_continue: bool = False, replan_after_turns: int | None = None, replan_after_todos: int | None = None, @@ -108,6 +109,10 @@ def __init__(self, config, *, profile: str, cwd: str, ): raise ValueError("turn_envelope requires a heartbeat worker") self.turn_envelope = turn_envelope + self.context_delivery = Execution( + mode="heartbeat" if profile.startswith("heartbeat-") else "plain", + context_delivery=context_delivery, + ).context_delivery if timeout_seconds <= PHASE_WORK_RESERVE_SECONDS: raise ValueError("Worker budget must exceed the 160s startup/settlement reserve") if not config.agent_model or not config.agent_effort: @@ -276,6 +281,7 @@ def _install_worker(self, backend, handle, log_dir, logger): scheduler_timeout_sec=self.timeout_seconds, task_entry=self.task_entry, turn_envelope=self.turn_envelope, + context_delivery=self.context_delivery, native_goal_continue=self.native_goal_continue, replan_after_turns=self.replan_after_turns, replan_after_todos=self.replan_after_todos, diff --git a/benchmark/runtime/worker.py b/benchmark/runtime/worker.py index 33aa49869e..df34425088 100644 --- a/benchmark/runtime/worker.py +++ b/benchmark/runtime/worker.py @@ -92,7 +92,8 @@ def heartbeat_body(env: dict[str, str], turn_id: str, *, native_goal: bool, command += ["--turn-instance-id", turn_id] if env.get("LOOPX_TURN_ENVELOPE", "0") not in {"0", "1"}: raise ValueError("LOOPX_TURN_ENVELOPE must be 0 or 1") - if env.get("LOOPX_TURN_ENVELOPE") == "1" or decision_handoff_socket is not None: + context_dir = env.get("LOOPX_CODEX_CONTEXT_DIR") if env.get("LOOPX_CONTEXT_DELIVERY") == "on" else None + if env.get("LOOPX_TURN_ENVELOPE") == "1" or decision_handoff_socket is not None or context_dir: if native_goal: raise ValueError("TurnEnvelope capture requires a host-owned heartbeat Turn") # Wake identity comes from this worker, never from model-authored text. @@ -103,6 +104,8 @@ def heartbeat_body(env: dict[str, str], turn_id: str, *, native_goal: bool, command += ["--turn-envelope"] if decision_handoff_socket is not None: command += ["--decision-handoff-socket", decision_handoff_socket] + if context_dir: + command += ["--context-delivery", "on", "--codex-context-dir", context_dir] result = subprocess.run( command, cwd=env["LOOPX_PROJECT"], @@ -382,6 +385,8 @@ def native_command(env, execution, stage, wake, session_wake): session_id=session_wake.session_id if session_wake else None, mcp_server=None, ) + if execution.context_delivery != "off": + command[-1:-1] = json.loads(env.get("LOOPX_CODEX_CONTEXT_TRUST_ARGS", "[]")) if stage == "plan": command[-1:-1] = ["-c", "features.goals=false"] return command @@ -409,6 +414,8 @@ def _phase_timeout(timeout: float | None, deadline: float | None) -> tuple[float def run_once(env: dict[str, str]) -> dict: + if env.get("LOOPX_CONTEXT_DELIVERY", "off") not in {"off", "on"}: + raise ValueError("LOOPX_CONTEXT_DELIVERY supports only off or on") if env.get("LOOPX_TURN_ENVELOPE", "0") not in {"0", "1"}: raise ValueError("LOOPX_TURN_ENVELOPE must be 0 or 1") execution = Execution( @@ -420,6 +427,7 @@ def run_once(env: dict[str, str]) -> dict: validation_command=json.loads(env.get("LOOPX_VALIDATION_COMMAND_JSON", "[]")), task_entry=env.get("LOOPX_TASK_ENTRY"), turn_envelope=env.get("LOOPX_TURN_ENVELOPE", "0") == "1", + context_delivery=env.get("LOOPX_CONTEXT_DELIVERY", "off"), ) stage = env.get("LOOPX_TASK_STAGE", "execute") if stage not in {"plan", "execute"} or (stage == "plan" and execution.task_entry != "loopx-planned"): @@ -435,6 +443,9 @@ def run_once(env: dict[str, str]) -> dict: wake.mkdir(parents=True, exist_ok=False) home = Path(env["LOOPX_CODEX_HOME"]) env = process_environment(home, base=env) + # Internal transport arguments are derived afresh, never ambient authority. + env.pop("LOOPX_CODEX_CONTEXT_DIR", None) + env.pop("LOOPX_CODEX_CONTEXT_TRUST_ARGS", None) env["LOOPX_TURN"] = turn_id receipt = { "turn_id": turn_id, @@ -445,6 +456,7 @@ def run_once(env: dict[str, str]) -> dict: if observation_producer is not None else {}), **({"turn_envelope": True, "turn_envelope_source": "explicit"} if execution.turn_envelope else {}), + **({"context_delivery": "on"} if execution.context_delivery != "off" else {}), "stage": stage, "home_scope": "trial", "ok": False, @@ -496,6 +508,23 @@ def run_once(env: dict[str, str]) -> dict: checkpoint_wake = checkpoint_context.enter_context(CheckpointWake(env, receipt)) else: checkpoint_wake = _InertCheckpointWake() + 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) + receipt["context_transport"] = {"available": True} + except Exception as exc: + # Optional presentation cannot block an otherwise admitted + # trial. Unsupported hooks/diagnostics 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": from benchmark.runtime.planning import render_planning_prompt, task_plan_packet diff --git a/benchmark/tests/test_native_checkpoint.py b/benchmark/tests/test_native_checkpoint.py index c3e85b19e7..cf378f3532 100644 --- a/benchmark/tests/test_native_checkpoint.py +++ b/benchmark/tests/test_native_checkpoint.py @@ -876,7 +876,8 @@ def forbidden_fork(*args, **kwargs): @pytest.mark.parametrize("turn_envelope", [None, False, True], ids=["legacy", "ordinary", "envelope"]) -def test_interrupted_cursor_requires_exact_unreleased_final_guard(tmp_path, monkeypatch, turn_envelope): +@pytest.mark.parametrize("context_option", [None, "codex_context_dir", "codex_context_root"]) +def test_interrupted_cursor_requires_exact_unreleased_final_guard(tmp_path, monkeypatch, turn_envelope, context_option): """A stopped command is eligible only at the declared pre-output boundary.""" import copy import shlex @@ -894,6 +895,9 @@ def test_interrupted_cursor_requires_exact_unreleased_final_guard(tmp_path, monk guard["turn_envelope"] = turn_envelope if turn_envelope is False: argv.remove("--turn-envelope") + if context_option: + guard[context_option] = "/fixture/context" + argv += ["--" + context_option.replace("_", "-"), guard[context_option]] command = shlex.join(["/bin/bash", "-lc", shlex.join(argv)]) item = {"type": "commandExecution", "id": "guard-call", "command": command, "cwd": str(tmp_path), "status": "failed", "exitCode": 137, "aggregatedOutput": None} @@ -925,15 +929,27 @@ def test_interrupted_cursor_requires_exact_unreleased_final_guard(tmp_path, monk with pytest.raises(ValueError, match="must be boolean"): _verify_interrupted_guard(turns, {**guard, "turn_envelope": invalid}) variants = [] + for option in ("--codex-context-dir", "--codex-context-root"): + altered = list(argv) + if option in altered: + altered[altered.index(option) + 1] = "/foreign/context" + else: + altered += [option, "/foreign/context"] + changed = copy.deepcopy(turns) + changed[-1]["items"][-1]["command"] = shlex.join(["/bin/bash", "-lc", shlex.join(altered)]) + variants.append(changed) for field, value in [("aggregatedOutput", "score already exposed"), ("cwd", "/elsewhere"), ("status", "completed"), ("command", shlex.join(["/bin/bash", "-lc", shlex.join(argv) + "; touch later"]))]: changed = copy.deepcopy(turns) changed[-1]["items"][-1][field] = value variants.append(changed) - for option, value in [("--turn-instance-id", "wrong-turn"), ("--decision-handoff-socket", "/tmp/other"), + for option, value in [("--context-delivery", "on"), ("--turn-instance-id", "wrong-turn"), ("--decision-handoff-socket", "/tmp/other"), ("--registry", "/other.json"), ("--goal-id", "other")]: altered = list(argv) - altered[altered.index(option) + 1] = value + if option in altered: + altered[altered.index(option) + 1] = value + else: + altered += [option, value] changed = copy.deepcopy(turns) changed[-1]["items"][-1]["command"] = shlex.join(["/bin/bash", "-lc", shlex.join(altered)]) variants.append(changed) diff --git a/benchmark/tests/test_native_checkpoint_handoff.py b/benchmark/tests/test_native_checkpoint_handoff.py index adfa2c5ace..17411246b0 100644 --- a/benchmark/tests/test_native_checkpoint_handoff.py +++ b/benchmark/tests/test_native_checkpoint_handoff.py @@ -593,7 +593,8 @@ def settle_work(result, expected): @pytest.mark.skipif(not os.environ.get("LOOPX_NATIVE_CHECKPOINT_BIN") or os.name != "posix", reason="Requires the real native CLI and private Unix transport") @pytest.mark.parametrize("capture", ["files", "native", "full", "off", "controller"]) -def test_actual_worker_handles_midwake_replan_and_duplicate_without_guard_replay(tmp_path, capture): +@pytest.mark.parametrize("context_delivery", [False, True]) +def test_actual_worker_handles_midwake_replan_and_duplicate_without_guard_replay(tmp_path, capture, context_delivery): """Use run_once, its renderer/listener/session owner and the actual quota CLI.""" import sys import tomllib @@ -601,12 +602,13 @@ def test_actual_worker_handles_midwake_replan_and_duplicate_without_guard_replay from benchmark.runtime.worker import run_once from tests.control_plane.test_quota_settlement_cli import ( AGENT_ID, GOAL_ID, TURN_ID, _write_fixture, _configure_autonomous_replan_fixture, - _heartbeat_receipt_count, _run_cli, + _heartbeat_receipt_count, _run_cli, _initialize_fixture_authority, ) if capture == "controller" and os.environ.get("LOOPX_CHECKPOINT_EXTERNAL_CONTROLLER") != "1": pytest.skip("Requires an explicitly attached host checkpoint supervisor") project, runtime, registry = _write_fixture(tmp_path / "authority") + _initialize_fixture_authority(project, runtime) logs = Path("/logs/agent/wakes") if capture == "controller" else tmp_path / "trial" / "wakes" candidate = project / "candidate.txt" candidate.write_text("original candidate") @@ -654,6 +656,23 @@ def response(payload, index, message): index_path = runtime / "goals" / GOAL_ID / "runs" / "index.jsonl" prior_runs = index_path.read_text() if index_path.exists() else "" _configure_autonomous_replan_fixture(project, runtime, registry) + # This fixture's later scenario is canonical too. Do not reseed a + # promoted store or make Markdown authoritative after cutover. + from tests.control_plane.canonical_authority_fixture import read_fixture_todos + from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection + projection = build_todo_runtime_shadow_projection(goal_id=GOAL_ID, + todos=read_fixture_todos(project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md"), + handoff_mode="soft_claim") + provider = Path(__file__).resolve().parents[2] / "loopx/control_plane/coordination/local_authority_provider.ts" + script = (f"import {{openLocalAuthorityStore}} from {json.dumps(provider.as_uri())};" + "let raw='';for await(const part of process.stdin)raw+=part;const v=JSON.parse(raw);" + "const s=await openLocalAuthorityStore(v.root,v.goal);const h=await s.loadAuthority();" + "const r=await s.commitAuthority({expected_provider_revision:h.provider_revision," + "operation_id:'fixture-midwake-scenario',events:[],receipts:[],next_projection:v.projection});" + "if(r.status!=='applied')throw Error(JSON.stringify(r));") + subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module", "-e", script], + input=json.dumps({"root": str(runtime), "goal": GOAL_ID, "projection": projection}), + text=True, capture_output=True, check=True, timeout=30) # Preserve the actual preflight settlement evidence. The reusable # fixture replaces the index; its two later synthetic observations # must not erase the already settled original Host Turn. @@ -700,12 +719,16 @@ def response(payload, index, message): LOOPX_CODEX_HOME=str(tmp_path / "worker-home"), LOOPX_SHARED_SKILLS=str(skills), MODEL_NAME="fixture", REASONING_EFFORT="high", OPENAI_BASE_URL=base_url, OPENAI_API_KEY="synthetic-local-only", LOOPX_CODEX_TURN_TIMEOUT_SEC="60") + if context_delivery: + env["LOOPX_CONTEXT_DELIVERY"] = "on" deadline = str(time.time() + 600) env["LOOPX_PHASE_DEADLINE_EPOCH"] = deadline if capture != "off": env["LOOPX_CHECKPOINT_REQUEST"] = str(request) result = run_once(env) assert result["ok"] and not result["timed_out"], result + if context_delivery: + assert result["context_transport"] == {"available": True} assert _heartbeat_receipt_count(runtime, TURN_ID) == 1 # Checkpoint preloading keeps the existing heartbeat contract without # injecting a Host countdown or runtime guidance. @@ -717,7 +740,7 @@ def response(payload, index, message): if capture == "off": assert "checkpoint_handoffs" not in result assert "--decision-handoff-socket" not in guards[0] - assert not list(logs.glob("*/decisions")) + assert bool(list(logs.glob("*/decisions"))) is context_delivery return assert len(result["checkpoint_handoffs"]) == 1 assert '--turn-envelope' not in shlex.split(guards[0]) diff --git a/benchmark/tests/test_sforge_runtime.py b/benchmark/tests/test_sforge_runtime.py index 80d7dcf496..caae5d1417 100644 --- a/benchmark/tests/test_sforge_runtime.py +++ b/benchmark/tests/test_sforge_runtime.py @@ -225,7 +225,8 @@ def command(handle, cmd, **kwargs): @pytest.mark.parametrize("capture_path", [None, "/control/checkpoint-request.json"]) -def test_checkpoint_request_reaches_sforge_worker_without_changing_default_profile(tmp_path, monkeypatch, capture_path): +@pytest.mark.parametrize("context_delivery", ["off", "on"]) +def test_checkpoint_request_reaches_sforge_worker_without_changing_default_profile(tmp_path, monkeypatch, capture_path, context_delivery): pytest.importorskip("sforge") pytest.importorskip("harbor") from sforge.harness.config import SForgeConfig @@ -235,7 +236,8 @@ async def installed(self, environment): pass # Exercise the real adapter handoff without starting a trial. monkeypatch.setattr(BenchmarkCodex, "install", installed) config = SForgeConfig(agent_model="fixture", agent_effort="xhigh") - worker = SForgeWorker(config, profile="heartbeat-explore", cwd="/task", checkpoint_request_path=capture_path) + worker = SForgeWorker(config, profile="heartbeat-explore", cwd="/task", checkpoint_request_path=capture_path, + context_delivery=context_delivery) worker.install_stop_hook(None, None, tmp_path, None) env = worker.runtime._worker_env(cwd="/task") profile = json.loads((tmp_path / "worker-profile.json").read_text()) @@ -246,6 +248,8 @@ async def installed(self, environment): assert "turn_envelope" not in profile assert "turn_envelope_source" not in profile assert "LOOPX_TURN_ENVELOPE" not in env + assert env.get("LOOPX_CONTEXT_DELIVERY") == ("on" if context_delivery != "off" else None) + assert profile.get("context_delivery") == (context_delivery if context_delivery != "off" else None) assert profile.get("checkpoint_capture_enabled") is (True if capture_path else None) with pytest.raises(ValueError, match="checkpoint_request_path"): SForgeWorker(config, profile="official", cwd="/task", checkpoint_request_path="/control/request.json") @@ -348,7 +352,7 @@ async def installed(self, environment): assert profile.get("native_goal_continue") is (True if enabled else None) with pytest.raises(ValueError, match="native-goal"): SForgeWorker(config, profile="heartbeat-resume", cwd="/task", native_goal_continue=True) - with pytest.raises(ValueError, match="boolean"): + with pytest.raises(ValueError, match="context_delivery"): SForgeWorker(config, profile="native-goal", cwd="/task", native_goal_continue="1") @@ -695,6 +699,7 @@ def test_envelope_rejects_incompatible_sforge_worker(profile): @pytest.mark.parametrize("profile,options", [ ("native-goal", ["--turn-envelope"]), + ("native-goal", ["--context-delivery"]), ("official", ["--replan-after-turns", "3"]), ("single", ["--replan-after-todos", "3"]), ("native-goal", ["--replan-after-todos", "3"]), @@ -1233,7 +1238,8 @@ def stop_before_solver(**kwargs): @pytest.mark.parametrize("capture", [False, True]) -def test_native_checkpoint_storage_guidance_reaches_both_prompt_surfaces(tmp_path, monkeypatch, capture): +@pytest.mark.parametrize("context_delivery", ["off", "on"]) +def test_native_checkpoint_storage_guidance_reaches_both_prompt_surfaces(tmp_path, monkeypatch, capture, context_delivery): pytest.importorskip("sforge") pytest.importorskip("harbor") from types import SimpleNamespace @@ -1253,6 +1259,7 @@ def test_native_checkpoint_storage_guidance_reaches_both_prompt_surfaces(tmp_pat monkeypatch.setattr(run, "RecordingDockerBackend", lambda **k: SimpleNamespace(image_exists=lambda image: True)) def before_solver(**kwargs): worker = kwargs["agent"] + assert worker.context_delivery is context_delivery assert worker.task_objective == "Original acceptance." assert worker.feedback_prompt.startswith(worker.workspace_instructions) assert worker.feedback_prompt.endswith("---\n\nOriginal acceptance.\n") @@ -1267,6 +1274,8 @@ def before_solver(**kwargs): args = ["--task", "fixture", "--tasks-dir", str(tmp_path), "--log-dir", str(tmp_path), "--run-id", "storage", "--worker", "heartbeat-explore", "--model", "fixture", "--effort", "xhigh", "--judge-url", "http://127.0.0.1:9999", "--feedback", "native"] + if context_delivery != "off": + args += ["--context-delivery", "on"] if capture: declaration = tmp_path / "request.json" declaration.write_text(json.dumps({"destination": "/logs/agent/checkpoints", @@ -1274,3 +1283,5 @@ def before_solver(**kwargs): args += ["--checkpoint-request", str(declaration)] with pytest.raises(RuntimeError, match="validated before model execution"): run.main(args) + receipt = json.loads((tmp_path / "runs/storage/fixture/runtime-receipt.json").read_text()) + assert receipt.get("context_delivery") == (context_delivery if context_delivery != "off" else None) diff --git a/benchmark/tests/test_shared_codex_runtime.py b/benchmark/tests/test_shared_codex_runtime.py index 7cf42f30db..09ec53859e 100644 --- a/benchmark/tests/test_shared_codex_runtime.py +++ b/benchmark/tests/test_shared_codex_runtime.py @@ -780,7 +780,8 @@ def test_default_execution_has_no_independent_turn_deadline(tmp_path): (False, "fresh_replan", None), (True, "fresh_replan", "explicit"), ]) -def test_harbor_install_receipt_keeps_capture_independent_from_display(tmp_path, monkeypatch, enabled, trigger, expected): +@pytest.mark.parametrize("context_delivery", ["off", "on"]) +def test_harbor_install_receipt_keeps_capture_independent_from_display(tmp_path, monkeypatch, enabled, trigger, expected, context_delivery): pytest.importorskip("harbor") from benchmark.runtime.harbor import BenchmarkCodex, CodexOffline from benchmark.runtime.checkpoint_wake import CheckpointWake @@ -798,6 +799,7 @@ def test_harbor_install_receipt_keeps_capture_independent_from_display(tmp_path, checkpoint_args = {"checkpoint_request_path": str(request), "checkpoint_request": request.read_bytes()} agent = BenchmarkCodex(logs_dir=tmp_path, model_name="fixture", turn_envelope=enabled, + context_delivery=context_delivery, **checkpoint_args) with CheckpointWake({"LOOPX_WAKE_LOG_DIR": str(wake_root), **({"LOOPX_CHECKPOINT_REQUEST": str(request)} if trigger else {})}, @@ -841,6 +843,8 @@ async def task_exec(**kwargs): assert agent._worker_env(cwd="/task")["LOOPX_USAGE_PING"] == "0" assert ("turn_envelope" in receipt) is (expected is not None) assert receipt.get("turn_envelope_source") == expected + assert receipt.get("context_delivery") == (context_delivery if context_delivery != "off" else None) + assert agent._worker_env(cwd="/task").get("LOOPX_CONTEXT_DELIVERY") == ("on" if context_delivery != "off" else None) if expected: assert receipt["turn_envelope"] is True @@ -1092,3 +1096,118 @@ def test_horizon_setting_rejects_non_boolean_and_non_loopx_mode(tmp_path, settin from benchmark.runtime.harbor import BenchmarkCodex with pytest.raises(ValueError, match="planning_horizon_goal"): BenchmarkCodex(logs_dir=tmp_path, model_name="fixture", **settings) + + +@pytest.mark.parametrize("mode", ["plain", "native-goal", "turn", "loopx-goal"]) +def test_native_context_opt_in_is_only_a_heartbeat_treatment(mode): + with pytest.raises(ValueError, match="context_delivery"): + Execution(mode=mode, context_delivery="on") + with pytest.raises(ValueError, match="context_delivery"): + Execution(context_delivery="1") + + +def test_context_transport_unavailable_preserves_ordinary_worker(tmp_path, monkeypatch): + from benchmark.tests.test_resume_sessions import resume_env + env = resume_env(tmp_path, monkeypatch) + env["LOOPX_CONTEXT_DELIVERY"] = "on" + def unavailable(env, home): + env["LOOPX_CODEX_CONTEXT_DIR"] = "not-ready" + env["CODEX_ROLLOUT_TRACE_ROOT"] = "not-ready" + raise ValueError("unsupported native host") + monkeypatch.setattr("benchmark.runtime.context_delivery.prepare_context_delivery", unavailable) + result = run_once(env) + assert result["ok"], result + assert result["context_delivery"] and result["context_transport"]["available"] is False + assert result["session"]["session_id"] + + +def test_context_transport_off_never_installs_or_trusts_ambient_args(tmp_path, monkeypatch): + from benchmark.tests.test_resume_sessions import resume_env + env = resume_env(tmp_path, monkeypatch) + env["LOOPX_CODEX_CONTEXT_DIR"] = "ambient-not-authority" + env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"] = '["--invalid-untrusted-override"]' + def unexpected(*args): + pytest.fail("disabled transport must not stage hooks or diagnostics") + monkeypatch.setattr("benchmark.runtime.context_delivery.prepare_context_delivery", unexpected) + result = run_once(env) + assert result["ok"] and "context_delivery" not in result and "context_transport" not in result + assert not (Path(env["LOOPX_CODEX_HOME"]) / "hooks.json").exists() + + +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) + with pytest.raises(ValueError, match="settings changed"): + prepare_codex_home(**(args | {"execution": Execution(mode="heartbeat")})) + + +@pytest.mark.parametrize("kind", ["public_directory", "symlink_directory", "existing_hooks"]) +def test_context_setup_preserves_unsafe_or_unowned_inputs(tmp_path, kind): + from benchmark.runtime.context_delivery import prepare_context_delivery + home = tmp_path / "home" + home.mkdir() + env = {"LOOPX_CLI": "loopx", "LOOPX_GOAL_ID": "fixture", "LOOPX_AGENT_ID": "agent"} + if kind == "existing_hooks": + protected = home / "hooks.json" + protected.write_text('{"operator": "unchanged"}') + else: + protected = home / "context-traces" + if kind == "public_directory": + protected.mkdir(mode=0o755) + protected.chmod(0o755) + else: + target = tmp_path / "outside" + target.mkdir() + protected.symlink_to(target, target_is_directory=True) + before = protected.read_bytes() if protected.is_file() else protected.lstat().st_mode + with pytest.raises(ValueError, match="private|changed within"): + prepare_context_delivery(env, home) + assert (protected.read_bytes() if protected.is_file() else protected.lstat().st_mode) == before + assert "LOOPX_CODEX_CONTEXT_TRUST_ARGS" not in env + + +def test_context_discovery_keeps_solver_configuration_and_unrelated_hooks(tmp_path, monkeypatch): + from contextlib import contextmanager + from benchmark.runtime.context_delivery import prepare_context_delivery + from loopx.control_plane.turn_driver.codex_stdio import StdioNativeGoalTransport + + home = tmp_path / "home" + home.mkdir() + config = home / "config.toml" + original_config = b"[features]\nplugins=true\nremote_plugin=true\nplugin_sharing=true\n" + config.write_bytes(original_config) + methods, commands = [], [] + + @contextmanager + def discovery(argv, **kwargs): + commands.append(argv) + def request(method, params): + methods.append(method) + if method == "initialize": + return {} + 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)] + 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) + + monkeypatch.setattr(StdioNativeGoalTransport, "spawn", discovery) + 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"] + 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"]) + assert "native-hash-0" in trusted[1] and "native-hash-1" in trusted[1] + assert "unrelated" not in trusted[1] and "never-trust-this" not in trusted[1] + 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 + + +@pytest.mark.parametrize("value", [True, False, "1", "codex_benchmark_heartbeat", "codex_app_heartbeat", "all"]) +def test_context_benchmark_setting_rejects_broad_or_other_host_activation(value): + with pytest.raises(ValueError, match="context_delivery"): + Execution(context_delivery=value) diff --git a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md index cb8c59ebbd..b5c08ce3d1 100644 --- a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md +++ b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md @@ -729,8 +729,8 @@ The next slice stays within the existing three convergence acceptances above: 3. **Retain full current authority and cold-start requirements.** Every response supplies fresh identity, action, admission, scope/approval/stop conditions, cancellation, selection, claim/lease, unresolved reads and settlement/scheduler - obligations. Only allowlisted source payloads are eligible for reuse; - current control results remain full even when their bytes are unchanged. + obligations. Registered complete source and packet projections are eligible for exact reuse; + current control results remain inline even when their bytes are unchanged. Reused requirement text remains binding, including its textual stop clauses; it is neither a summary nor optional background. Preserve complete original requirements in the reconstructed context. Treat @@ -757,26 +757,54 @@ App consumers, and their full fallback. A standalone diff helper is insufficient |---|---| | Baseline owner | The host tracks complete model-input delivery and context continuity. The typed projection validates the supplied binding and computes the delta; it cannot infer retention from storage. Unsupported hosts continue full delivery. No model self-attestation is required. | | Full capture | Retain the immutable complete current decision for audit, signatures and same-capture recovery. A delta is a presentation, never a new authority store or mutation patch against Goal state. | -| Delta unit | Initially compare JSON object subtrees exactly, preserving scalar types, missing versus null, empty values and full strings. Replace changed arrays atomically; do not infer element identity or equivalence from prose. State removals explicitly. Give base/target identities and digests once, with readable paths and changed values; avoid nested reference chains and instructions that require the model to reconstruct the entire JSON mechanically. | +| Delta unit | Compare complete registered JSON subtrees exactly, preserving scalar types, missing versus null, empty values and full strings. Arrays use edits at indices in the exact hashed baseline, including length changes; indices are transport coordinates, never inferred entity identities. Reorders remain exact or select full when smaller. Do not normalize timestamps/revisions or infer equivalence from prose. Use a hierarchical change tree with complete values, object changes/removals and array items/length, avoiding repeated long path prefixes. | | Consumer recovery | Missing or mismatched baseline, skipped/out-of-order delta or truncated delivery returns the full current projection from the original capture, without rerunning admission. A lost delta is never acknowledged as the next baseline. Reset the baseline after full delivery; bound incremental ancestry with a full refresh rather than an ever-growing patch chain. Source freshness and authority are still checked before execution; presentation recovery cannot revive a stale or cancelled decision. | | CLI adoption | Keep the ordinary quota entrypoint and existing capture/detail route. A host that can observe the active model context passes the optional baseline binding at that transport boundary. A bare CLI with no continuity observer returns full context; it must not silently trust a previous output file. Exact argument/wire shape follows caller characterization, not a new command family. | | Turn adoption | Consume the same full/delta context owner while retaining Turn signatures and outer-executor effects. Host-only settlement execution remains distinct from a manual CLI agent's executable settlement obligations. | | Choice of representation | Emit a delta only when it is smaller after binding/removal metadata. Otherwise emit full context. Do not truncate values or change source selection to meet a byte target. | -**Source and change boundaries.** Start with registered complete Goal narrative, -canonical acceptance requirement bodies and selected Todo text/notes, retaining -their exact strings and source identity. Eligibility comes from the existing -source adapter and typed field contract, never a classifier of sentence meaning. -Keep current source revision, completeness, failures, status/claim, user gates, -adoption and other execution-control fields in the fresh portion. An unknown or -mixed source without a qualified split stays full. Give each known source slot -a stable key derived from its existing source kind and canonical entity identity; -ambiguous or duplicate identities fall back to full. Do not compare the whole -`sources` array as one unit: a changing authority revision must not resend an -unchanged requirement body. Inside a source payload, arrays remain atomic in -the first version. A changed field is replaced completely, including any late -constraint; deleted fields have explicit removal paths. The owner reconstructs -and compares against the complete target before releasing a delta. +**Source and change boundaries.** The shared `context_delivery_v1` presentation +covers whole quota packets, whole Turn envelopes and their common work context. +The surface is part of the existing Goal/Agent/session/epoch/capture binding, so +one surface cannot borrow another surface's baseline. This extends presentation, +not admission, persistence or settlement authority. Registered complete packet +projections can reuse unchanged subtrees. Unknown root fields stay fresh. + +Current identity, action, admission, scope/approval/stops, cancellation, claims, +source health, pending reads, quota and scheduler facts stay inline. Settlement +identity, commands, step order, conditions and attachment requirements also stay +inline. Static preconditions, authoring schemas and optional attachment guidance +can be reused exactly; they remain binding, not optional because omitted from a +warm wire. The same rule covers existing vision-audit instructions while its +current judgment stays inline. Unknown fields within these control owners stay +fresh. There is no second copy of the fresh controls in the delta. + +Work context retains registered complete Goal narrative, canonical acceptance +bodies and selected Todo text/notes. The Explore owner registers exact evidence, +guidance and authoring metadata; orchestration gates, branch selection, pagination, +current source revisions and unknown fields remain fresh. Source explanations, +context-read instructions and the Todo read-model's field-name descriptor may be +reused without dropping the actual Todo fields, current revision or provenance. +An unknown or mixed source without a qualified split stays fresh; ambiguous source +identities and incomplete sources select full. Changed strings remain complete, +including late constraints. Evidence statuses and timestamps are never ignored. + +The previous body-only split, atomic changed arrays and per-leaf path lists were +an initial implementation, not a limit on the accepted efficiency goal. The +current codec forces fresh fields into one hierarchical change tree and computes +exact edits for the rest. Array growth, shrinkage, reorder, duplicates and null +are structural changes against that specific baseline, with no fuzzy matching. +The owner validates the expected tree and reconstructs the exact immutable target +before delivery. Omitting an unchanged live control is rejected even if the +remaining patch could otherwise reconstruct correctly. Corrupt, lost, mismatched +or more expensive deltas use the same-capture full representation automatically. + +Count cold full envelopes, bindings, repeated controls, change-tree metadata, +changed values and recovery reads in the comparison. A narrow-body percentage +cannot estimate whole-packet reuse; offline retained-baseline bytes still do not +establish actual solver input savings or model outcomes. The existing projection +RPC is the only preparation/recovery interface; both transports use the same TS +codec, and Python remains a thin adapter. Production host adoption stays open. **First real consumer and the compaction race.** The current native Codex CLI session adapter observes session identity; its capture/handoff release occurs @@ -885,6 +913,389 @@ host's final input assembly boundary for each consumer, with the shared typed projection following that qualified seam. This checkpoint keeps phase one open and does not reinterpret synthetic usage counters as token or outcome savings. +**Small lifecycle hooks and capture-file recovery (architecture refinement for review).** +Keep the negative native-hook results above as evidence about bulk hook injection, +not a dependency that blocks the shared implementation. A lifecycle event can +invalidate a baseline without transporting a large requirement document. The +proposed recovery route is an ordinary complete read of the immutable capture; +a recovery control message may therefore precede the complete source read. +Dependent business work still requires the complete current source. This refines +the earlier requirement to place all recovered text in the first post-compaction +model request; it adds no owner question, admission or debit. + +| Owner | Responsibility and boundary | +|---|---| +| Existing quota/Turn producer | Compute the full authoritative result once and persist the immutable capture before publishing a presentation. Preserve the existing required-source, signature, selection and settlement owners. | +| Shared TypeScript context projector | Bind Goal/Agent/session/epoch, capture and digest; compare registered source bodies; keep current control/source facts fresh; choose smaller exact delta or full. Reconstruct against the complete target before release. | +| Codex lifecycle adapter | Observe startup/resume/fork/compaction and invalidate prior continuity observations. Resume may keep an unconfirmed candidate only; fresh actual-input evidence is required for reuse. Carry a small event and capture recovery reference, never the complete packet. Hook configuration/trust and event coverage must be qualified on each actual host. | +| Existing local capture/detail IO | Read the original capture in bounded pages, preserving complete text and validating target identity/digest. A preview or successful file write does not establish complete consumption. No new guard or quota operation is needed to recover presentation. | + +**Sequence and failure handling.** First use or unknown continuity selects full. +Only an intact, observed delivery in the same session/epoch may become the warm +baseline; a stored file alone cannot. Generate the next presentation against that +baseline while retaining the current full capture. If the epoch moves during +preparation, discard the pending delta and use full recovery. If the event arrives +after publication, its small recovery reference points to that same current +capture. A lost/truncated delta does not advance the confirmed baseline. Bound +ancestry and force periodic full refresh; do not accumulate unbounded patch chains. + +The recovery wrapper catches primary-handler errors and emits a bounded full-read +reference from the saved capture. Missing event observations or unknown wrapper +health invalidate warm eligibility at the next ordinary entrypoint. Retry only +presentation IO; never repeat admission or business effects. A missing/corrupt +capture retains the existing source-recovery failure rather than fabricating a +baseline. Treat event detection, intact delivery and complete file consumption as +separate evidence. Recovery guidance does not become a new enforced user gate. + +**Implementation and qualification checkpoint.** The shared projector now has +full/delta preparation and same-capture reconstruction through the existing Turn +RPC, reused for ordinary quota context. Exact duplicate selected-Todo titles are +elided only when the complete body is identical; distinct titles and source state +remain intact. Real File/SQLite quota tests cover ordinary CLI and Codex App +profiles, shared Turn source content, epoch loss, corrupt delta, unchanged source +state and zero extra spend. This is a shipped-code proposal, not an enabled native +warm adapter or installed App delivery. + +The ordinary `quota should-run` capture rendezvous now connects that codec to +JSON stdout, for both quota and `--turn-envelope`. A qualified caller writes a +private `delivery-request.json` beside the held `decision.json`, containing the +existing `scope`, optional `baseline` and `retained` inputs. It opts in by adding +`context_delivery: true` to the existing digest-bound release. The CLI binds the +Goal/Agent to the actual decision, saves immutable `projection.json`, and returns +the shared full/delta presentation with a path and byte digest for full recovery. +Missing, malformed, cross-identity or unavailable optimization input returns full; +it does not rerun admission or change the decision's success status. Legacy +releases and ordinary invocations keep their original output and file behavior. + +Real File/SQLite subprocess tests cover cold/warm output, epoch reset, malformed +request, cross-Agent refusal, source preservation, original receipt and zero +spend. Recovery uses the exact saved projection, including after the candidate +delta was prepared. This closes the CLI producer seam only. The rendezvous is +still before native output processing: a release does not ACK model delivery, +and callers without a qualified continuity observer must not opt into warm reuse. +Both actual solver and installed App lifecycle consumers remain unqualified. + +The recovery reference now carries a runnable `quota context-read` command. +This is local capture IO under the existing quota entrypoint, not another +projection or decision owner. Pass the original `--capture-file` and +`--capture-sha256`; follow each `next_command` until `eof`, combining `content` +pages in byte-offset order. `--page-bytes` bounds UTF-8 content (256–8192 bytes, +4096 by default); JSON metadata and host wrapping also count toward output cost. +Each page verifies the complete file digest and object before returning content. +Replacement, bad offsets or unavailable captures return an error without partial +context or quota side effects. Parent directory aliases remain usable; substituted +final symlinks and non-regular files are refused. The command works without Goal +or runtime initialization and never executes a guard, admission or debit. Page +EOF means only that file's end, not proof of model consumption or permission to +reuse a baseline. This transport-local `context_capture_page_v1` shape does not +extend control-plane lifecycle state. + +The native transport probe uses the actual shared projector, bounded tool reads +and a small `SessionStart(compact)` reference. Its deterministic local provider +requests the reads; it does not prove an autonomous solver follows the recovery +route. Preserve the earlier bulk-hook and fail-open negatives. Qualify complete +first read, warm delta, compaction, primary-handler failure and full target read +from outgoing model inputs. Include every recovery page in cost and call counts; +a smaller hook or packet alone is not a saving. The next integration is the +Codex lifecycle adapter consuming the ordinary quota capture/detail path for +both phase-one consumers, without a second runner or a new approval flow. + +The composed native fixture now executes the real `quota should-run` CLI from +Codex tools, using SQLite, its private rendezvous and the shared projector. It +observes complete paged output in actual outgoing model input before supplying +a warm baseline. Codex exec covers ordinary quota; app-server covers the Turn +envelope. Both recover the same immutable CLI projection after automatic +compaction, including primary recovery-handler failure, without a third guard +or quota debit. This closes a producer-to-native transport test gap; the input +oracle independently inspects the deterministic loopback provider's input. +The source text, source state and original captures are +checked independently of the delta. All pages, including recovery, remain part +of the cost boundary. These reads now execute `quota context-read`, including +digest checks, rather than ad-hoc file slicing. Installed adoption and autonomous +outcomes remain open. + +The existing CLI rendezvous also accepts an optional `native_input` cross-check: +`trace_bundle`, `tool_call_id`, `capture_file` and `capture_sha256`. Its native +Codex trace-v1 reader verifies the exact full baseline in the input of the +inference that produced the still-active, model-originated `exec_command` call. +It requires a matching session, ordered trace, completed response containing +that call, and either an intact full tool result or every digest-bound context +page. Earlier inference history, partial pages, previews, other item roles, +completed calls and implicit server-side history do not qualify. Code-mode +`exec_command` children additionally require one still-live native cell whose +model-visible call id and exact JavaScript match the current inference response. +The complete body/pages must appear in actual model-visible custom-tool text +blocks, directly or in the native exec result envelope; private child results +and unforwarded output never qualify. Missing, malformed, oversized or +incompatible observations discard the retention claim and choose full automatically; ordinary callers are unchanged. +The reader only consumes already enabled private traces, with the existing +transport size bound. It reads a bounded event-log tail rather than the whole +session; if the active inference is outside that window it selects full. It +does not enable diagnostics or change native sessions. +Python owns this provider-specific IO; the shared TS projector still validates +baseline identity, freshness and exact reconstruction. + +This check can narrow a caller's retention claim, never mint one or establish +future dispatch ordering. A full baseline may be reused while every original +byte is observable; this reader does not infer a patch chain from saved files. +The native CLI/SQLite composition uses this reader for warm selection, with +an independent loopback oracle. The production consumer now provides +`quota context-hook --codex-context-dir ` for native events +on stdin. Ordinary `quota should-run` uses the same `--codex-context-dir` with +its existing capture destination and calls the shared projector directly; +a test-side rendezvous controller is no longer required. The Python module owns +only Codex-specific event/private-file IO, not a new capability, runner, Goal +state or decision owner. TS retains delta, binding and fallback semantics. + +The caller prepares a separate private directory per native session/workspace +and registers that hook command for `SessionStart`, `PreToolUse` and `PreCompact` +through the native configuration/trust flow. The CLI does not install hooks, +alter trust or enable tracing. It checks the current `CODEX_THREAD_ID`, working +directory and active call and requires complete input from already enabled +traces; absent, incompatible or uncertain evidence selects full automatically. +Removing the opt-in argument restores the ordinary path. Existing arguments and +legacy rendezvous behavior remain unchanged; selection commands carry the same +transport argument without another human step. + +Startup, fork and compact events invalidate the epoch. A same-session/workspace +resume clears the active call but retains an unconfirmed candidate and its epoch; +this is a lookup hint, never a continuity receipt. The new still-active call must +independently prove the entire baseline in its actual model input. Missing or +pruned input still selects full. Multiple native bundles for a resumed session +are resolved only by a unique valid current-call/input observation, never by +mtime or the latest saved file; ambiguous or excessive discovery selects full. +Same-session recovery carries only the latest immutable capture's complete-read command; +a different session never borrows the old reference. Preparation and publication +of the latest recovery reference share a private lock, so post-publication +compaction also recovers that capture. A primary journal write failure still +returns a known recovery reference; a publication that cannot record its reference +returns the original full payload. A candidate file is never an ACK: complete +model input is cross-checked on each reuse. A complete projection read through +`context-read` can establish the baseline body; `1` and `1.0` are both JSON +numbers, while booleans remain distinct. Missing/mismatched baseline, source +permission and freshness rules are unchanged. + +Native exec/quota and app-server/Turn tests now execute this production CLI and +hook rather than a test lifecycle controller. They cover complete cold read, +warm delta, automatic post-publication compaction, primary-handler write failure, +same-capture complete recovery and zero extra guard/debit. The deterministic +loopback provider still does not prove an autonomous model follows recovery or +achieves lower total cost. The same production path is also exercised through +native app-server code-mode for both quota and Turn views, including the parent +cell/child binding, forwarded output, compaction and primary-write failure. +Missing, mismatched or ended cells and incomplete custom-tool output remain full. +This qualifies the native code-mode transport, not installed desktop adoption. +The following persistent recovery route qualifies wholly unexecuted/killed hooks; +installed adoption, trace/all-read overhead and real solver outcomes remain separate. No runtime/owner +question is added, and phase one remains open. + +The existing benchmark worker now consumes this production path through an +explicit `context_delivery: "on"` heartbeat option (default `"off"`), also exposed by EdgeBench/SForge +as `--context-delivery on`. Its private trial home stages the product hooks and +uses native `hooks/list` to trust only their exact hashes for that process; +unrelated workspace hooks remain untrusted. Opt-in private traces support the +input observer. Setup failure continues ordinary full output; an existing +checkpoint handoff keeps its original capture ownership. Selection appears in +run/install/wake receipts, while setup availability is not a retention receipt. +Default runs add no hooks, trace or trust configuration, and changing treatment +requires a new trial. The product renderer and recovery pages preserve the +original Turn, capture and pinned CLI route. Deterministic native tests drive +the actual worker with both quota and Turn views through cold read, warm delta, +automatic compaction and complete same-capture recovery without extra guard or +debit. A second real worker invocation and a second code-mode app-server process +now resume the same native session, demonstrate intact prior input and select +warm delta without another cold read, for both quota and Turn views. A subsequent +native compaction still recovers the original full capture. Each benchmark wake +uses a private trace subdirectory so earlier wakes do not enter its hot-path +diagnostic scan. These deterministic checks qualify cross-wake transport, +not net solver savings. Harbor/container and Codex App installation and complete solver/cost +qualification remain open; lost-hook recovery is qualified below. +Native discovery, trace IO and all recovery reads belong in that cost comparison. + +Matched native input replay now separates the subtree codec from the read policy: +compare full and delta under the same page size, forwarding and batching policy, +then compare read policies separately. Count all repeated model input (including +history and tool definitions), model requests, CLI reads and private trace bytes. +A cold-plus-warm pair can save fewer bytes than a long warm series. Smaller pages +batched into fewer model rounds can increase CLI calls while reducing repeated +input; do not attribute that combined gain to the codec alone. Deterministic +replay over captured inputs qualifies transport cost proxies, not autonomous +solver outcomes, billed tokens, cache savings or installed adoption. + +Code-mode callers can forward successful CLI stdout directly without serializing +another exec-result wrapper. They may group bounded reads only when every page +survives the **final model-facing output**, including the host's separate cap. +A larger requested output budget alone is insufficient: the native host can omit +text blocks or truncate inside otherwise valid JSON. Parse success and page EOF +therefore do not establish intact delivery. Verify contiguous UTF-8 offsets and +the original complete digest; reduce a failed batch and reread the same original +capture rather than readmit. No universal batch count or increased default limit +is qualified by a particular replay. Keep truncation negatives alongside the +successful cost comparison, and include recovery in any subsequent promotion. + +The production quota recovery instruction and native resume/compact hooks now +share that bounded-read guidance, including direct successful stdout forwarding, +small adaptive batches and same-capture retries. A malformed model-visible page +contributes no retention evidence. It does not permanently poison a subsequent +complete reread still present in the current input. The native observer accepts +overlapping ranges after a page-size change only when each intact range matches +the original digest-bound bytes and their union covers the complete capture. +Gaps, previews alone and a different capture remain insufficient; no history +clearing, new acknowledgement or admission is needed. Composed code-mode tests +exercise batched stdout, overlapping read boundaries, cross-process resume, +warm delta and native compaction recovery for both quota and Turn views. + +A lost event handler now has a separate **read-only recovery entry**, not a +second execution/admission path. `quota context-read --codex-context-dir DIR +--goal-id GOAL --agent-id AGENT` selects the latest original capture only when +the private journal matches the ambient native session, current workspace and +requested Goal/Agent. It reads no current Goal, evaluates no guard, spends no +quota and does not change the journal. A missing/foreign/older unscoped journal +refuses lookup; an existing explicit digest-bound reference remains usable. +Lookup starts at offset zero. Every following `next_command` pins the original +path and hash, so a concurrent newer publication cannot mix captures. Missing, +changed or symlinked files retain the ordinary reader's refusal behavior. + +The experimental switch is now `--context-delivery {off,on}`, default `off`. +It reuses the existing declared host context: `--codex-app` / the equivalent +`codex_app_heartbeat` profile, or the benchmark worker's existing `generic_cli` +profile plus its explicit `LOOPX_EXECUTION_MODE=heartbeat` and +`LOOPX_CONTEXT_DELIVERY=on` declarations. The worker's existing execution owner +rejects non-heartbeat modes and propagates these settings to native tools; its +scheduler declaration is unchanged. No process or ambient native-session +heuristic authenticates a host. With `on`, unsupported, absent, incomplete or +contradictory run-mode declarations fail the invocation with a nonzero exit and +an actionable reason before admission or transport writes, including hooks. +Correct the declaration or use `off`; this adds no owner question or approval. +Within a supported run mode, missing or invalid retained-input evidence still +automatically restores the same full capture. Hooks, configuration, saved presentation and +reentry preserve this binary selection. Directory options or a host handoff +alone cannot turn on deltas. Full original-capture recovery remains usable when +off. These caller declarations select a supported adapter path, grant no +execution authority and do not replace actual-input retention verification. + +`quota context-config --context-delivery on --codex-app` renders the native hook/recovery configuration through +the CLI; the benchmark worker reuses that same provider helper. Shared Codex App +configuration uses `--codex-context-root` to isolate journals by native session, +while dedicated trial homes retain `--codex-context-dir`. Read-only lookup never +creates an unknown session directory. The heartbeat bootstrap preserves the +root/capture options and uses the existing per-wake `LOOPX_TURN` binding instead +of persisting a Turn ID. Native hook trust and private actual-input diagnostics +remain host-owned; missing diagnostics select full. Generated configuration +installs nothing, changes no trust and grants no execution authority. See +[activation/readback/rollback](../../heartbeat-automation-prompt.md#opt-in-native-context-delivery). +This qualifies generated configuration plus native consumption, not installed +desktop adoption or a new cost measurement. + +Native context delivery can also share the existing checkpoint handoff capture. +The checkpoint owner holds/verifies the original full decision and releases its +exact digest before native projection. Lost/refused release publishes no optimized +result; interruption retains original full continuation, without another guard. +Native interrupted-cursor verification binds the context route as well as the +existing identity/capture routes. The host-side preflight retains the complete +original capture; the Darwin native-checkpoint limitation is unchanged. Deterministic native tests +cover ordinary held release, warm reuse, lost-hook compaction recovery and +midwake capture/refusal. Resumed worker tests use the latest generated guard and +settle the independently checked prior readback before a new Turn; old history +cannot stand in for a new admission. This is transport composition, not a new +cost/outcome qualification or permission to modify frozen trials. + +Saved preflight presentation now uses public `quota context-project`: the host +pins the original path/hash and Goal/Agent/Turn, then the native tool invokes the +same projector/actual-input observer without another guard or debit. A compact +descriptor supplies full/delta read commands, the available target and an +original full fallback. Cold complete context remains verifiable; uncertain +native state or optional cache failure selects full. Source hash/scope failure +refuses before publication. Bounded physical-file/hash IO is shared with the +existing reader. Native tests prove complete warm reconstruction through the +real checkpoint worker's generated saved-read entry across resumed wakes, +without guard replay. Existing +decision freshness, checkpoint ordering and default-off full preloading remain; +total-cost/solver-outcome qualification must include the added descriptor and +tool reads, not only delta bytes. + +A matched deterministic native worker diagnostic now includes the generated +preflight, resumed history and every page, using the same synthetic requirement +and default function-tool read policy in both execution orders. Across two +two-wake trials per arm, serialized-input token estimates fall 35.83% and reader +calls fall from 72 to 48. Cold input increases about 11%; savings come from warm +reuse. Elapsed ranges overlap, so this establishes no latency improvement. +Both arms enable diagnostic capture; it does not qualify tracing cost against +uninstrumented execution, autonomous solver outcomes or exact billing. The +separate real-capture replay remains a different workload and read policy. + +A subsequent three-arm diagnostic at runtime source `912d56036` compares +ordinary tracing-off, ordinary tracing-on and opt-in delta/tracing-on execution. +Three counterbalanced two-wake trials per arm include generated preflight, +resumed history, descriptors, persistent instructions and every 4096-byte serial +read. Against ordinary tracing-off, delta reduces the summed serialized-input +o200k proxy from 5,482,641 to 3,499,312 (36.17%), whole-request proxy from +5,892,161 to 3,801,600 (35.48%), and requests from 114 to 84. Reader calls fall +from 108 to 72, with six additional projection calls. All trials verify complete +original bytes, independent reconstruction, session continuity and unchanged +guard/debit counts. Tokenization runs outside timing; synthetic loopback usage +does not establish billing or autonomous solver outcomes. + +The default-off comparison exposes an IO tradeoff: delta retains about 5.77 MB +of native diagnostics per two-wake trial, versus zero without tracing. Native +background plugin-catalog clones dominate the variable total retained footprint; +endpoint file sizes are not cumulative writes. Timing, CPU and net IO savings +remain unqualified. Hook identity discovery now disables plugin features only +in its temporary native process, avoiding unrelated catalog startup. It still +asks native `hooks/list` for hashes and trusts only the exact owned hooks; +solver configuration, default-off execution and model settings are unchanged. +Real native config readback verifies this isolation, repeated discovery returns +the same trust identities, and discovery sends no inference request. The same +test detects the old helper's enabled plugin features. The cost figures above +precede this helper fix and do not measure its latency or disk savings. + +The shared codec also avoids copying unchanged/discarded replacement subtrees +and canonicalizing identical requirement strings. A V8 capture identifies +comparison, sizing and cloning work; a controlled intervention replays 31 fixed +real captures through the loaded owner. Four counterbalanced process pairs, +each with 12 measured replays after warmup, reduce mean prepare/recovery time +from 347.30 to 303.06 ms (12.74%). Every emitted delivery remains byte-identical, +and reconstruction plus detached-value checks preserve both source captures. +This measures the loaded codec on one host, excluding startup, CLI/backend IO, +native host, model and whole-Turn time. It changes neither packet bytes nor the +input-token result; complete-cost and solver-outcome acceptance remain open. + +The native adapter now uses the existing Core original-capture IO owner for +shared read guidance and projection, removing the reverse Core-to-CLI dependency. +CLI presentation dispatch remains ahead of authority IO; event recording, +settlement readback and memory ingestion keep their original order. Native +input parsing, call binding and digest-bound page validation have separate +responsibilities without weakening lifecycle or complete-coverage checks. +The existing legacy quota adapter supplies its own field binding to both codec +operations; caller JSON cannot nominate additional reusable controls. Unknown +fields stay fresh. These changes preserve the measured runtime contract rather +than introduce another projection or compatibility reader. + +The opt-in benchmark worker carries a compact recovery instruction in native +`developer_instructions`, independently of SessionStart/PreToolUse delivery. +Native code-mode consumers can carry the same product-owned instruction in +their isolated native configuration; append it to existing host instructions +rather than replacing unrelated policy. Its conditional trigger is continuing +a captured decision after context loss, not starting an ordinary new turn. +It directs same-capture reading before dependent work without a user question; +independent authorized work remains possible and recovery renews neither authority +nor freshness. The first lookup page supplies the existing bounded-read guidance. +Default-off workers add no persistent instruction. Deterministic native tests +remove the complete source during real compaction and then omit or kill every +subsequent hook process. The actual worker and code-mode host recover all bytes +through the persistent public CLI route without another guard or debit. Private +files/session identity still establish no retention claim; actual current input +remains the independent reuse criterion. Include the persistent instruction and +all recovery reads in future cost comparisons; prior measurements exclude this +new instruction. This does not claim live desktop installation or autonomous +solver effectiveness. + +Capture regression qualification also distinguishes settlement binding from +closeout: an upgraded receipt with an outstanding semantic replan still permits +an inert replan capture and grants no delivery authority. Explicit pending +closeout suppresses that capture. The real CLI fixture and the typed selector +now test both facts instead of inferring closeout from the receipt status. + Qualification compares matched normal, replan and wait/recovery workloads with optional capability modes unchanged. Include first delivery, warm reuse, reset, deletion, array reordering, lost/duplicated/out-of-order deltas, partial output, diff --git a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md index bdf46f4136..0bf7b7a00b 100644 --- a/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md +++ b/docs/architecture/rfcs/agent-loop-effect-interpreter-v0.zh-CN.md @@ -494,8 +494,8 @@ live 模型结果、App 采用或以下三项收敛验收完成。 删除被替代投影。输出释放前的 host 传输 ACK 既不证明模型完整收到了内容,也不证明其仍被 保留。现有 capture/handoff 是接入位置,并非已经实现的连续性契约。 3. **保留完整当前权限事实与冷启动要求。** 每次响应重新给出身份、动作、准入、范围/审批/停止 - 条件、取消、选择、claim/lease、未完成必读及结算/scheduler 义务。只有明确允许的来源正文 - 可以复用,当前控制结果即使字节未变也完整交付。复用的要求正文仍具有完整约束力,包括其中 + 条件、取消、选择、claim/lease、未完成必读及结算/scheduler 义务。注册的完整来源及 packet + 投影可以精确复用,当前控制结果即使字节未变也完整交付。复用的要求正文仍具有完整约束力,包括其中 的文字停止条件;它不是摘要或可选背景。重建后的上下文保留全部原始 要求。先在结构化 owner 理清混合 Todo 历史和验收解释,再改变必读范围;时间、owner、状态、 标题、长度均不能证明过时。把必读正文放到引用后再全读回来不是节省,替换累计备注是状态 @@ -513,20 +513,40 @@ live 模型结果、App 采用或以下三项收敛验收完成。 |---|---| | 基线归属 | host 跟踪模型输入的完整交付和上下文连续性;typed 投影核验所提供的绑定并计算 delta,不能从磁盘推断保留状态。不支持的 host 继续全量交付,不要求模型自行声明“仍然记得”。 | | 完整捕获 | 保留不可变的当前完整决策,供审计、签名与同次捕获恢复。delta 只是展示,不成为新的权威存储,也不用于修改 Goal 状态。 | -| 增量单位 | 首版精确比较 JSON 对象子树,保留标量类型、缺失与 null、空值及完整字符串;数组改变时整段替换,不从正文猜测元素身份或等价性。删除须显式表达。基线/目标身份及 digest 只给一次,并附可读路径和变化值;避免嵌套引用链,也不要求模型机械重建整份 JSON。 | +| 增量单位 | 精确比较注册的完整 JSON 子树,保留标量类型、缺失与 null、空值及完整字符串。数组按精确摘要所绑定的基线索引编辑,并表达长度变化;索引只是传输坐标,不是推测的实体身份。重排须精确恢复,若全量更小则选全量。不忽略时间戳/revision,也不从正文猜等价性。使用分层变化树表达完整值、对象变化/删除及数组项目/长度,避免重复长路径前缀。 | | 消费恢复 | 基线缺失/不匹配、跳过/乱序 delta 或输出截断,从原始 capture 返回完整当前投影,不重新准入。丢失的 delta 不能确认为下一基线;完整交付后重设基线,以全量刷新限制增量祖先链,避免无限累积 patch。执行前仍核验来源新鲜度与权限;展示恢复不能重新授权过时或已取消的决策。 | | CLI 采用 | 保持普通 quota 入口和现有 capture/detail 路径;能观察活跃模型上下文的 host 在传输边界提供可选基线绑定。没有连续性观察者的裸 CLI 返回全量,不能静默信任上次输出文件。具体参数/wire 形式在调用方刻画后确定,不新增命令族。 | | Turn 采用 | 消费同一完整/增量上下文 owner,同时保留 Turn 签名和外层 executor 效果归属。host 独占的结算执行与手动 CLI agent 的可执行结算义务仍有区别。 | | 表示选择 | 计入绑定和删除元数据后,只有更小才发送 delta,否则发送全量。不为字节目标截断值或改变来源选择。 | -**来源与变化边界。** 首批面向注册的完整 Goal 叙述、canonical 验收要求正文及选中 Todo 的 -正文/备注,保留精确字符串与来源身份。可复用范围由现有 source adapter 和 typed 字段契约 -确定,不分类句子含义。当前来源版本、完整性、失败、status/claim、User gate、目标采用及 -其他执行控制字段仍每次完整给出。不能可靠拆分的未知或混合来源保持全量。每个已知来源槽 -按既有来源种类和 canonical 实体身份生成稳定 key,身份重复或不明确时回退全量。不能将整个 -`sources` 数组作为比较单位,否则 authority revision 变化会迫使未变要求全文重发。来源正文 -内部的数组在首版仍整体替换;字段改变时完整替换,包括迟到的约束,删除使用显式路径。 -owner 在释放 delta 前重建并与完整目标逐项比较。 +**来源与变化边界。** 共享的 `context_delivery_v1` 展示覆盖完整 quota packet、 +完整 Turn envelope 及其公共 work context。surface 纳入既有 Goal/Agent/session/epoch/ +capture 绑定,不同展示不能借用彼此基线。扩展的是展示,不改变准入、持久化或结算 authority。 +注册的完整 packet 投影可精确复用未变子树,未知根字段仍每轮完整交付。 + +当前身份、动作、准入、范围/批准/停止条件、取消、claim、来源健康、待完成读取、quota +及 scheduler 事实仍每轮给出。结算身份、命令、步骤顺序、条件和附件要求也保持当前完整。 +静态前置说明、编写 schema 和可选附件 guidance 可精确复用;温态 wire 未重发,不减弱 +它们的约束力。既有 vision audit 的说明适用相同规则,当前判断仍每轮给出。这些控制 owner +内的未知字段保留每轮交付。delta 不再重复携带另一份相同的当前控制字段。 + +work context 保留注册的完整 Goal 叙述、canonical 验收正文和选定 Todo 文字/备注。 +Explore owner 注册精确证据、guidance 和编写元数据;编排 gate、分支选择、分页、当前 +来源 revision 和未知字段继续每轮交付。来源解释、上下文读取指令以及 Todo read-model +字段名描述可复用,不删除实际 Todo 字段、当前 revision 或来源依据。未知或混合来源若无 +合格 split 则完整交付;来源身份不明确或读取不完整时选全量。变更字符串仍完整替换,包含 +迟到约束;不忽略证据状态或时间戳。 + +此前仅正文 split、变化数组整段替换及逐叶重复路径是初步实现,不是效率目标的上限。 +当前 codec 将必发字段直接放进同一分层变化树,其余部分计算精确变化。数组增删、重排、 +重复值和 null 都针对该精确基线做结构转换,不模糊匹配。owner 校验预期变化树并重建 +不可变完整目标后才交付;即使其余 patch 能重建正确,漏掉未变的当前控制字段仍会被拒绝。 +损坏、丢失、基线不匹配或更贵的 delta 自动采用同 capture 全量表示。 + +比较计入冷启动全量 envelope、绑定、必发控制、变化树元数据、变化值及恢复读取。 +狭窄正文比例不能代表整包复用;假定基线保留的离线字节结果仍不证明真实 solver 输入节省 +或模型成果。准备/恢复继续使用既有 projection RPC,两种传输共用同一个 TS codec, +Python 仅作薄 adapter;生产 host 采用验收继续保留。 **首个真实调用方与压缩竞态。** 当前原生 Codex CLI session adapter 只观察会话身份, capture/handoff 的 release 发生在 stdout 交付之前,两者都不提供所需的模型输入连续性 @@ -606,6 +626,271 @@ oracle 检查实际发出的模型输入,而非 hook 日志或 stdout。这只 最终输入组装边界证明这一集成,再让共享 typed 投影采用已验证的接缝。本检查点保持 一期未完成,不将合成 usage 计数解释为 token 或成果收益。 +**轻量生命周期 hook 与捕获文件恢复(架构修订,待 review)。** 上述原生 hook +负例继续作为整包 hook 注入的证据,不应成为共享实现的前置阻塞。生命周期事件可以 +使基线失效,而不传输大型要求正文。恢复通过普通入口完整读取不可变捕获;因此, +恢复控制消息可以先于完整来源读取,依赖该来源的业务工作仍须等完整来源读取后执行。 +这修订了此前“压缩后第一条模型请求必须装入全部恢复正文”的要求,不增加 owner +问询、准入或扣账。 + +| Owner | 职责与边界 | +|---|---| +| 既有 quota/Turn producer | 只计算一次完整权威结果,先持久化不可变捕获,再发布展示形式。保留既有必读来源、签名、选择和结算 owner。 | +| 共享 TypeScript 上下文投影 | 绑定 Goal/Agent/session/epoch、capture 和摘要;比较已注册来源正文;每次保留新鲜控制与来源事实;选择较小的精确差分或全量,发布前按完整目标重建核对。 | +| Codex 生命周期 adapter | 观察 startup/resume/fork/compaction,使先前连续性观察失效;resume 只可保留未确认候选,复用仍须新的实际输入证据。只携带小事件和捕获恢复引用,不传整个 packet。每种真实宿主分别验证 hook 配置、信任与事件覆盖。 | +| 既有本地 capture/detail IO | 有边界地分段读取原捕获,保留完整正文,核对目标身份和摘要。预览或写文件成功不证明完整消费。恢复展示不需要新 guard 或 quota 操作。 | + +**时序与失败处理。** 首次使用或连续性未知时走全量;只有同会话、同 epoch 内 +完整且可观察的交付才能成为温态基线,文件存在本身不够。依据该基线生成下一展示, +同时留存当前完整捕获。生成期间 epoch 改变则丢弃待发布差分,走全量恢复;事件在 +发布后到达时,小型恢复引用指向同一份当前捕获。丢失或截断的 delta 不推进确认基线。 +限制差分继承深度并定期全量刷新,不积累无限 patch 链。 + +恢复 wrapper 捕获主 handler 错误,从保存的捕获生成有界的全文读取引用。事件观察 +缺失或 wrapper 健康未知时,在下一次普通入口取消温态资格。只重试展示 IO,不重放 +准入或业务效果。捕获缺失/损坏继续走既有来源恢复失败,不能伪造基线。事件检测、 +完整交付与完整文件消费分别保留证据;恢复提示不能变成新增的强制用户问询门槛。 + +**实现与验证检查点。** 共享投影已通过既有 Turn RPC 提供 full/delta 准备和同捕获 +重建,也复用于普通 quota 上下文。仅当完整正文逐字相同时省略重复的 selected-Todo +标题,保留不同标题及来源状态。真实 File/SQLite quota 测试覆盖普通 CLI、Codex App +模式、共享 Turn 来源内容、epoch 丢失、差分损坏、来源状态不变和零额外扣账。这是 +产品代码提案,还不是已启用的原生温态 adapter 或安装态 App 交付。 + +普通 `quota should-run` 的 capture rendezvous 现已把该 codec 接到 JSON stdout, +同时支持 quota 与 `--turn-envelope`。已获资格的调用方在被保留的 `decision.json` +旁写入私有 `delivery-request.json`,沿用 `scope` 及可选的 `baseline`、`retained` +输入,并在既有摘要绑定的 release 中添加 `context_delivery: true` 显式启用。 +CLI 将 Goal/Agent 绑定到实际决策,保存不可变的 `projection.json`,返回共享全量/ +差分展示及完整恢复文件的路径、字节摘要。优化输入缺失、损坏、身份不符或不可用时 +返回全量,不重跑准入或改变原决策成功状态。旧 release 与普通调用的输出、文件行为 +保持不变。 + +真实 File/SQLite 子进程测试覆盖首次/温态输出、epoch 重置、损坏请求、跨 Agent +拒绝、来源保留、原 receipt 和零扣额。差分准备后仍可从精确保存的投影恢复。 +这只关闭 CLI producer 接缝;rendezvous 仍位于原生输出处理之前,release 不等于 +模型交付 ACK,没有合格连续性观察器的调用方不得启用温态复用。实际 solver 与 +安装态 App 的生命周期消费者仍未获得资格。 + +恢复引用现包含可直接执行的 `quota context-read` 命令。这是既有 quota 入口下的 +本地 capture IO,不另建投影或决策 owner。传入原始 `--capture-file` 和 +`--capture-sha256`,依次执行每页 `next_command` 到 `eof`,按字节 offset 顺序组合 +`content`。`--page-bytes` 限制 UTF-8 内容大小(256–8192 字节,默认 4096);JSON +元数据和 Host 包装同样计入输出成本。每页返回内容前校验完整文件摘要及 JSON object。 +文件被替换、offset 错误或 capture 不可用时返回错误,不交付部分上下文或产生 quota +副作用。父目录别名仍可使用;拒绝被替换为符号链接的最终文件及非普通文件。该命令 +无需初始化 Goal 或 runtime,不执行 guard、准入或扣额。分页 EOF 只表示文件结尾, +不证明模型完整消费,也不授予基线复用资格。传输局部的 `context_capture_page_v1` +形状不扩展控制面生命周期状态。 + +原生传输 probe 使用实际共享投影、有界工具读取和很小的 `SessionStart(compact)` +引用。确定性的本机 provider 请求相应读取,不证明自主 solver 会遵循恢复路线。 +保留之前整包 hook 与 fail-open 负例;从实际发出的模型输入验证首次完整读取、温态 +差分、压缩、主 handler 失败和完整目标读回。成本与调用次数须包含每个恢复分段, +hook 或 packet 变小本身不等于节省。下一接入是让 Codex 生命周期 adapter 消费 +既有普通 quota capture/detail 路径,覆盖一期两个调用方,不增加第二套 runner 或审批流程。 + +组合原生 fixture 现通过 Codex 工具执行真实 `quota should-run` CLI,使用 SQLite、 +私有 rendezvous 和共享投影;在实际发出的模型输入中观察到完整分页输出后,才提供温态 +基线。Codex exec 覆盖普通 quota,app-server 覆盖 Turn envelope。两条路径均在自动 +压缩后恢复同一份不可变 CLI 投影,包含主恢复 handler 失败,且不触发第三次 guard 或 +quota 扣额。这补齐 producer 到原生传输的测试缺口;独立 oracle 检查确定性 loopback +provider 的实际输入。来源正文、来源状态和原始 capture 独立于 +delta 校验。包括恢复在内的全部分页仍属于成本边界;这些读取现执行带摘要校验的 +`quota context-read`,不再自行编写文件切片。安装采用和自主成果保持未完成。 + +既有 CLI rendezvous 还接受可选的 `native_input` 交叉校验,字段为 `trace_bundle`、 +`tool_call_id`、`capture_file`、`capture_sha256`。原生 Codex trace-v1 reader 核对 +产生当前仍在执行的、由模型发起的 `exec_command` 调用的那次推理输入,是否包含精确 +全量基线;要求 session 匹配、trace 顺序有效、已完成 response 包含该调用,并观察到 +完整工具结果或全部摘要绑定的 context 分页。更早的推理历史、部分分页、预览、其他 +消息角色、已完成调用及隐式服务端历史均不具备资格。code-mode 的 `exec_command` +子调用还须绑定唯一仍在执行的原生 cell,其模型可见 call id 与完整 JavaScript 必须 +匹配当前推理 response。完整正文/分页须实际出现在模型可见 custom-tool 文本块中, +可以是正文或原生 exec 结果封装;私有子调用结果及未转发的输出不具备资格。观察缺失、 +格式错误、超限或不兼容时,丢弃保留声明并自动全量;普通调用方保持不变。 +Reader 只读取已经启用的私有 trace,复用既有传输大小上限;读取有界事件日志尾部, +不读整份 session,当前推理已超出该窗口时自动全量。不启用诊断或改变原生 session。 +Python 持有 provider 特有的 IO;基线身份、新鲜度和精确重建仍由共享 TS +投影器校验。 + +该检查只能收窄调用方的保留声明,不能自行生成声明或证明未来 dispatch 的顺序。 +只有每个原始字节仍可观察时,才可复用全量基线;reader 不从已存文件推断 patch 链。 +原生 CLI/SQLite 组合使用该 reader 选择温态交付,loopback provider 保留为独立 +oracle。当前生产接入新增 `quota context-hook --codex-context-dir `, +从 stdin 读取原生事件;普通 `quota should-run` 在既有 capture destination 上携带同一 +`--codex-context-dir`,直接调用共享 projector,无需测试侧 rendezvous controller。 +该 Python 模块只负责 Codex 特有的事件和私有文件 IO,不新增 capability、runner、 +Goal 状态或决策 owner;TS 继续持有差分、绑定和回退语义。 + +调用方为每个原生 session/workspace 准备独立的私有目录,并通过原生配置与信任流程 +把上述 hook 命令接到 `SessionStart`、`PreToolUse`、`PreCompact`。CLI 不安装 hook、 +修改信任或启用 tracing。它核对当前 `CODEX_THREAD_ID`、工作目录和 active call,并 +要求已经启用的 trace 提供完整输入;缺失、不可兼容或无法确认时自动全量。移除 opt-in +参数即可恢复原路径;普通参数和旧 rendezvous 行为保持不变。selection 命令继续携带 +同一 transport 参数,不增加人工步骤。 + +启动、fork、压缩事件使 epoch 失效。同 session/workspace 的 resume 清除 active call, +只保留未确认候选及其 epoch;这是查找线索,不是连续性回执。新的仍在执行的调用必须 +独立证明完整基线存在于实际模型输入;输入缺失或裁剪仍全量。同 session 的多个原生 +bundle 只由唯一有效的当前调用/输入观察选择,不使用 mtime 或最新文件;歧义或发现 +范围超限仍全量。同一 session 的恢复提示只带最新不可变 +capture 的完整读取命令,跨 session 不借用旧引用。生成候选和保存最新恢复引用使用同一 +私有锁;发布之后的压缩也能恢复同 capture。主 journal 写入失败时,handler 仍返回已知 +恢复引用;生产发布无法记录引用时返回原全量。候选文件从不等于 ACK,完整模型输入需在 +每次复用时重新核对。`context-read` 读回的完整投影可以提供基线正文证据;`1` 与 `1.0` +均属 JSON number,布尔值仍须不同。基线缺失或错配、来源权限与新鲜度规则不变。 + +原生 exec/quota 与 app-server/Turn 测试现执行上述生产 CLI 和 hook,而非测试侧 +生命周期 controller;覆盖完整初读、温态差分、发布后自动压缩、主 handler 写入失败、 +同 capture 完整补读和零额外 guard/扣额。它们仍使用确定性 loopback provider,不证明 +真实模型自主遵循恢复提示或总成本收益。同一生产路径也通过原生 app-server 的 +code-mode 验证 quota 与 Turn 两种视图,覆盖父 cell/子调用绑定、输出转发、压缩和 +主 journal 写入失败。cell 缺失、错配或结束以及输出不完整时仍全量。这验证原生 +code-mode transport,不等于已安装桌面产品采用。下述常驻恢复入口验证整个 hook 未执行/ +被杀;安装采用、trace 与全部补读开销及真实 solver 成果仍须独立验证;不开新 runtime/owner +问询门,一期保持开放。 + +既有 benchmark worker 现通过显式 `context_delivery: "on"` heartbeat 选项消费上述生产 +路径(默认 `"off"`);EdgeBench/SForge 以 `--context-delivery on` 暴露同一选择。trial 私有 home 放置 +产品 hook,并用原生 `hooks/list` 获取精确 hash,仅在该进程信任这些 hook;无关 +workspace hook 仍不受信任。显式启用的私有 trace 支持输入 observer。设置失败继续 +普通全量,既有 checkpoint handoff 保留原 capture 所有权。run/install/wake receipt +记录选择,设置可用不等于保留回执。默认运行不新增 hook、trace 或 trust 配置;改变 +treatment 须另起 trial。产品 renderer 与恢复分页保留原 Turn、capture 和固定 CLI +路由。确定性原生测试驱动真实 worker,覆盖 quota 与 Turn 两种视图的完整初读、温态 +差分、自动压缩及同 capture 完整恢复,零额外 guard/扣额。第二次真实 worker 调用和 +第二个 code-mode app-server 进程现恢复同一原生 session,核验先前完整输入,直接 +选择温态差分而不再初读;quota 与 Turn 两种视图均覆盖。之后的原生压缩仍恢复原始 +全量 capture。benchmark 每次 wake 使用独立私有 trace 子目录,旧 wake 不进入热路径 +诊断扫描。这些确定性检查验证跨唤醒传递,不证明 solver 净收益。Harbor/container 与 Codex App +安装与完整 solver/成本资格仍开放;hook 丢失恢复在下文验证。成本比较须计入原生发现、 +trace IO 和所有补读。 + +同负载原生输入重放现将子树 codec 与读取策略分开:先在相同分页大小、转发与批量 +策略下比较全量和差分,再单独比较读取策略。统计全部重复模型输入(含历史与工具 +定义)、模型请求、CLI 读取和私有 trace 字节。一次初读加一次温态续跑,节省比例 +可能小于长期温态序列。将更小的页合并到更少模型轮次,可能增加 CLI 调用却减少重复 +输入;不能把组合收益全部归给 codec。对已捕获输入的确定性重放只验证传输成本估算, +不证明自主 solver 成果、计费 token、缓存收益或安装采用。 + +code-mode 调用方可以直接转发成功的 CLI stdout,避免再次序列化 exec 结果包装。 +只有每一页都完整到达**最终模型输入**时才可合并有界读取;还须考虑宿主的独立上限。 +单独调高请求的输出预算并不足够:原生宿主可能省略文本块,或在仍可解析的 JSON +内部截断内容。因此 JSON 解析成功和分页 EOF 都不证明传递完整。须验证连续 UTF-8 +偏移及原始完整摘要;批次失败时缩小批次并重读同一原 capture,不重新准入。单次 +重放不能资格化通用批次数或提高默认上限。保留截断负例,并在后续推广成本中计入恢复。 + +生产 quota 恢复指令与原生 resume/compact hook 现共用这份有界读取指导,包含直接转发 +成功 stdout、小批量自适应读取及同 capture 重试。损坏的模型可见页不贡献保留证据, +也不会永久否决当前输入中随后完整的重读。分页大小改变后,原生 observer 仅在每个 +完整区间都匹配原摘要绑定的字节、且区间并集覆盖整个 capture 时接受重叠读取。 +缺口、只有预览或不同 capture 仍不足以证明保留;无需清空历史、新增 ACK 或重新准入。 +组合 code-mode 测试覆盖批量 stdout、重叠分页边界、跨进程续跑、温态差分及原生压缩 +恢复,包含 quota 和 Turn 两种视图。 + +事件 handler 完全丢失时,现有独立的**只读恢复入口**,不新增执行或准入路径。 +`quota context-read --codex-context-dir DIR --goal-id GOAL --agent-id AGENT` +仅在私有 journal 匹配环境中的原生 session、当前工作目录和所请求 Goal/Agent 时, +选取最新的原 capture。它不读取当前 Goal、不执行 guard、不扣额,也不修改 journal。 +缺失、跨身份或旧版未标注范围的 journal 拒绝隐式查找;已有显式摘要绑定引用仍可使用。 +查找从 offset 零开始,后续每个 `next_command` 固定到原路径和 hash,避免并发发布 +更新 capture 时混读。文件缺失、内容变化或符号链接继续沿既有 reader 拒绝。 + +实验开关现为 `--context-delivery {off,on}`,默认 `off`。复用已有声明的宿主上下文: +`--codex-app` / 等价 `codex_app_heartbeat` profile,或 benchmark worker 既有 +`generic_cli` profile 加显式 `LOOPX_EXECUTION_MODE=heartbeat` 与 +`LOOPX_CONTEXT_DELIVERY=on` 声明。worker 既有执行 owner 拒绝非 heartbeat 模式, +并把这些设置传递给原生工具;scheduler 声明不变。不通过进程名或环境中的原生 +session 启发式认证宿主。`on` 时,跑法声明缺失、不完整、 +矛盾或不受支持会在准入与 transport 写入前以非零退出码和可操作原因报错,hook 也遵守 +此规则。应修正声明或改为 `off`,不新增 owner 问询或审批。受支持跑法内,输入保留证据 +缺失或失效仍自动恢复同一 capture 的全量。hook、配置、已保存 +包展示和重入均传递这一二态选择。目录参数或 host handoff 本身不能开启差分。关闭时 +仍可回查原 capture 全量。这些调用方声明只选择受支持 adapter 路径,不授予执行权限, +也不替代实际输入保留验证。 + +`quota context-config --context-delivery on --codex-app` 通过 CLI 生成原生 hook/恢复配置,benchmark worker 复用同一 +provider helper。Codex App 共享配置使用 `--codex-context-root` 按原生 session 隔离 +journal,独立 trial home 保留 `--codex-context-dir`。只读查找不创建未知 session +目录。心跳 bootstrap 保留 root/capture 参数,使用既有每次唤醒的 `LOOPX_TURN` +绑定,不持久化固定 Turn ID。原生 hook 信任和私有实际输入诊断仍由宿主管理;缺少 +诊断时使用全量。生成配置不安装文件、不改变信任、不授予执行权限。参见 +[启用/读回/回滚](../../heartbeat-automation-prompt.md#opt-in-native-context-delivery)。 +本段验证生成配置及原生消费,不等于真实桌面安装采用或新的成本测量。 + +原生上下文交付也可复用既有 checkpoint handoff 的同一 capture。checkpoint owner +先持有并核验原始完整决策,按精确摘要释放后才执行原生投影。释放丢失或拒绝不发布 +优化结果;中断时保留原始全量 continuation,不再次 guard。原生中断 cursor 核验 +除既有身份/capture 路由外,也绑定 context 路由。宿主侧 preflight 保留完整原始捕获, +Darwin 原生 checkpoint 限制保持不变。确定性原生测试覆盖普通持有释放、warm 复用、 +hook 丢失后的压缩恢复,以及唤醒中途捕获/拒绝。续跑 worker 测试读取最新生成的 +guard,在新 Turn 前通过正常入口结算独立核验的前次读回;旧历史不能替代新准入。 +这只验证传输组合,不构成新的成本/结果资格,也不授予修改冻结 trial 的权限。 + +已保存 preflight 的交付现走公开 `quota context-project`:宿主固定原始路径/摘要和 +Goal/Agent/Turn,原生工具调用同一 projector/实际输入 observer,不再次 guard 或扣额。 +简短描述返回 full/delta 读取命令、可用的 target 和原始全量回退。冷启动全文仍可核验; +原生状态不确定或可选 cache 失败选择全量,来源摘要/作用域失败则在发布前拒绝。 +物理文件和摘要的有界 IO 与既有 reader 复用。原生测试通过真实 checkpoint worker +跨唤醒恢复后生成的 saved-read 入口验证 warm 完整重建及零 guard 重放。原决策新鲜度、checkpoint +顺序和默认关闭时的全量预载保持;完整成本/solver 结果资格须计入新增描述及工具读取, +不能只计算 delta 字节。 + +新增匹配的确定性原生 worker 诊断,计入生成的 preflight、跨唤醒历史和全部分页; +两侧使用相同合成要求及默认 function-tool 读取策略,并覆盖正反执行顺序。每侧两组 +双唤醒中,序列化输入 token 估算下降 35.83%,reader 调用从 72 次降为 48 次。 +冷启动输入约增加 11%,收益来自 warm 复用;耗时区间重叠,未证明延迟改善。 +两侧均开启诊断捕获,不据此认定相对未采集诊断时的追踪成本、自主 solver 成果或精确 +计费。既有真实捕获回放是另一种负载及读取策略,分别保留。 + +后续以运行时源 `912d56036` 对照普通关闭 trace、普通开启 trace、显式差分并开启 +trace 三组路径,每组按交叉顺序执行三次双唤醒。计入生成 preflight、恢复历史、描述、 +常驻指令及全部 4096 字节串行读取。相对普通关闭 trace,差分累计序列化输入 o200k +代理值从 5,482,641 降为 3,499,312(36.17%),整请求代理值从 5,892,161 降为 +3,801,600(35.48%),请求数从 114 降为 84;reader 调用从 108 降为 72,另增六次 +投影调用。全部样本核验原包完整字节、独立重建、session 连续性及不变的 guard/扣额 +次数。分词在计时之外完成;合成 loopback usage 不证明计费或自主 solver 成果。 + +默认关闭对照也揭示 IO 代价:差分每次双唤醒保留约 5.77 MB 原生诊断,未追踪时为零。 +原生后台插件目录克隆占据大部分波动的总保留量;终点文件大小不等于累计写入。 +延迟、CPU 和净 IO 收益仍未验收。hook 身份查询现仅在临时原生进程关闭插件功能, +避免启动无关目录;仍由原生 `hooks/list` 生成哈希,且只信任精确自有 hook。 +solver 配置、默认关闭路径和模型设置保持不变。真实原生配置回读核验该隔离,重复查询 +返回相同信任身份,且不发送推理请求;同一测试能够发现旧 helper 仍开启插件功能。 +上述成本数据采于本次 helper 修复之前,不代表其延迟或落盘收益。 + +共享 codec 同时避免复制未变化或未选中的替换子树,以及对相同要求字符串重复规范化。 +V8 采样定位比较、尺寸计算及复制开销;受控修改使用 31 份固定真实 capture 回放已加载 +owner。四组交叉进程对照,每进程预热后测 12 次回放,prepare/recovery 平均耗时从 +347.30 降至 303.06 ms(12.74%)。所有交付输出逐字节一致;重建与值隔离核验保持两份 +来源不被修改。这只测单机已加载 codec,不含启动、CLI/backend IO、原生宿主、模型或 +完整 Turn;不改变 packet 字节或输入 token 结果,完整成本和 solver 成果验收仍开放。 + +原生 adapter 改为复用既有 Core 原始 capture IO owner 的共享读取指导与投影,消除 +Core 反向依赖 CLI。CLI 展示命令仍在权限 IO 之前分派;事件记录、settlement 回读与 +memory ingest 保持原顺序。原生输入解析、调用绑定和摘要绑定分页核验分清职责, +不削弱生命周期或完整覆盖检查。既有旧 quota adapter 向编码与恢复两侧提供自己拥有 +的字段绑定;调用方 JSON 不能指定更多可复用控制字段,未知字段仍为 fresh。这些调整 +保持已测运行契约,不新增另一套投影或兼容读取入口。 + +显式启用的 benchmark worker 将简短恢复指令放入原生 `developer_instructions`, +独立于 SessionStart/PreToolUse 的交付。原生 code-mode 消费者也可在隔离配置中携带 +同一产品 owner 生成的指令;应追加到既有宿主指令,不替换无关规则。触发条件是 +上下文丢失后继续一个已捕获的决策,不是每个普通新 turn 的前置检查。指令要求依赖 +该上下文的工作先读回同 capture,不增加用户问询;独立授权工作仍可继续,恢复不刷新 +权限或新鲜度。首个查找页提供既有的有界读取指导。关闭功能的 worker 不增加常驻 +指令。确定性原生测试在真实压缩中删除完整来源,随后跳过或杀死所有后续 hook 进程, +由真实 worker 与 code-mode 宿主通过常驻公开 CLI 入口读回全部字节,零额外 guard/扣额。 +私有文件与 session 身份仍不构成保留声明;是否可复用仍独立核验实际当前输入。 +后续成本比较须计入常驻指令与全部恢复读取,先前测量不包含这条新指令。此处不宣称 +真实桌面安装采用或自主 solver 效果。 + +Capture 回归验证同时区分 settlement 绑定与 closeout:receipt 已升级但仍欠语义 +重规划时,可以捕获无执行效果的 replan,且不授予交付权限;显式 pending closeout +则抑制该捕获。真实 CLI fixture 和 typed selector 现分别验证这些事实,不再从 +receipt status 推断 closeout。 + 在可选能力模式不变的条件下,比较匹配的普通执行、重规划和等待/恢复负载,覆盖首次交付、 温态复用、reset、删除、数组重排、丢失/重复/乱序 delta、部分输出、来源/身份不匹配及迟到 要求变化。重建结果须等于完整当前投影,fresh 控制事实始终不得省略。统计首次和补读的模型输入、上下文占用、 diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 93eba1e99a..7788a2ee6b 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -421,6 +421,105 @@ loopx-canary heartbeat-prompt \ --goal-id ``` +### Opt-in native context delivery + +`--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: + +| 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) | +| 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 +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 +recovers the same original full capture automatically. `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. +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. + +For a trusted Codex host, create private context/capture directories and render +the native setup through the installed CLI: + +```sh +loopx --format json quota context-config --context-delivery on --codex-app \ + --codex-context-root --goal-id --agent-id +``` + +The read-only result contains `native_config.hooks` and +`native_config.developer_instructions`. Merge the hooks into the host's existing +`hooks.json` or inline hook configuration, and append the recovery instruction to +its existing developer instructions. Preserve other configuration. Review and +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. + +Generate the normal bootstrap with `heartbeat-prompt --thin --codex-app +--bootstrap`, its existing Goal/Agent/registry arguments, and +`--context-delivery on --decision-output-root --codex-context-root +`; optionally add `--turn-envelope`. The loader preserves +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. + +Read back native hook discovery/trust and the generated guard options, then +verify cold full, warm delta only after complete current-input evidence, +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. +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 +rules. If interrupted while held, consume the original full continuation packet. + +For a host that already saved the quota decision before invoking Codex, present +that immutable snapshot from the native tool instead of running its guard again: + +```sh +loopx --format json quota context-project --context-delivery on --codex-app \ + --capture-file --capture-sha256 \ + --goal-id --agent-id --turn-instance-id \ + --codex-context-root --decision-output-root +``` + +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. Invalid optional +transport/cache falls back to that same full capture; invalid source hash or +Goal/Agent/Turn refuses publication. This is integrity-checked presentation of +the caller's snapshot, not fresh authority, registry lookup, installation, +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 +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. +Existing captures remain private recovery evidence. This opt-in does not grant +execution authority or prove installed App adoption or net token savings. + ## Template Generate the persistent prompt from the installed runtime: diff --git a/loopx/cli_commands/quota.py b/loopx/cli_commands/quota.py index d84984529f..df42d85377 100644 --- a/loopx/cli_commands/quota.py +++ b/loopx/cli_commands/quota.py @@ -4,8 +4,13 @@ import time from collections.abc import Callable, Mapping from pathlib import Path +from typing import Any -from .quota_capture import bind_capture_selection_transport, capture_decision, handoff_decision, prepare_decision_capture +from .quota_capture import ( + bind_capture_selection_transport, capture_decision, handoff_decision, + prepare_decision_capture, handle_native_context_command, +) +from ..control_plane.turn_driver.decision_capture import project_captured_context from ..usage_goal import observe_quota_result from ..capabilities.explore.composition_frontier import ( @@ -463,11 +468,16 @@ def _emit_quota_result( turn_id=_effective_spend_turn_instance_id(payload, heartbeat_turn_id=heartbeat_turn_id), started_at=usage_quota_started, ) + from .quota_capture import native_context_activation + + context_delivery_enabled = payload.get("ok") is True and native_context_activation(args)["enabled"] bind_capture_selection_transport(payload, args) + delivery_request = None + decision_identity = ("", "") try: capture_decision(capture_directory, payload) if payload.get("ok") is True: - handoff_decision(capture_directory, getattr(args, "decision_handoff_socket", None)) + delivery_request = handoff_decision(capture_directory, getattr(args, "decision_handoff_socket", None), context_delivery_enabled) except QuotaDecisionTransportError as exc: # The guard already ran. Report that typed fact with the durable receipt # and saved decision, never a new collection failure that could replace @@ -477,6 +487,11 @@ def _emit_quota_result( ) print_payload(failure, args.format, _quota_renderer(args)) return 1 + if (delivery_request is not None or getattr(args, "codex_context_dir", None) + or getattr(args, "codex_context_root", None)): + agent = payload.get("agent_identity") + decision_identity = (str(payload.get("goal_id") or ""), + str(agent.get("agent_id") or "") if isinstance(agent, Mapping) else str(args.agent_id or "")) payload = _project_quota_cli_payload( payload, args, detail_sections, context.scheduler_context if context is not None else None, @@ -491,8 +506,95 @@ def _emit_quota_result( payload, registry_path=registry_path, ) + succeeded = bool(payload.get("ok")) + # Handoff releases the immutable original before any presentation. Native + # actual-input observation owns its projection when explicitly selected; + # never wrap one context-delivery wire in a second projection. + native_context = context_delivery_enabled and bool(getattr(args, "codex_context_dir", None) or getattr(args, "codex_context_root", None)) + if (args.format == "json" and succeeded and delivery_request is not None and not native_context + and context_delivery_enabled): + payload = project_captured_context(payload, directory=capture_directory, + request=delivery_request[0], capture_id=delivery_request[1], + goal_id=decision_identity[0], agent_id=decision_identity[1], + surface="turn_envelope" if getattr(args, "turn_envelope", False) else "quota_packet") + if (args.format == "json" and succeeded and capture_directory + and native_context): + import hashlib + from ..control_plane.turn_driver.codex_context import codex_context_directory, project_codex_context + + try: + capture_id = hashlib.sha256((capture_directory / "decision.json").read_bytes()).hexdigest() + directory = codex_context_directory(directory=args.codex_context_dir, + root=getattr(args, "codex_context_root", None), create=True) + 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) + except (OSError, ValueError): + pass # Lost optional capture: keep the admitted full payload, never rerun its guard. print_payload(payload, args.format, _quota_renderer(args)) - return 0 if payload.get("ok") else 1 + return 0 if succeeded else 1 + + +def _record_non_guard_quota_event( + payload: dict[str, Any], args: argparse.Namespace, *, registry_path: Path, + runtime_root_arg: str | None, context: QuotaCommandContext | None, goal_ref: dict[str, str] | None, + rollout_todo_id: str | None, rollout_replan_obligation_id: str | None, + spend_turn_instance_id: str | None, rollout_details: dict[str, Any], + append_cli_rollout_event: RolloutEventAppender, +) -> None: + """Record the CLI event before settlement readback and optional memory ingest.""" + append_cli_rollout_event( + payload, + registry_path=registry_path, + runtime_root_arg=runtime_root_arg, + event_kind=QUOTA_EVENT_KINDS[args.quota_command], + goal_ref=goal_ref, + agent_id=args.agent_id, + todo_id=rollout_todo_id, + run_id=( + spend_turn_instance_id + if args.quota_command == "spend-slot" + else None + ), + status=str( + payload.get("effective_action") + or payload.get("decision") + or payload.get("mode") + or args.quota_command + ), + summary=( + f"quota {args.quota_command} decision=" + f"{payload.get('decision') or payload.get('mode')} " + f"state={payload.get('state') or ''}" + ), + details=rollout_details, + allow_failed=args.quota_command == "should-run", + ) + if args.quota_command == "spend-slot" and spend_turn_instance_id: + assert context is not None # A settlement requires a prepared authority context. + attach_spend_settlement_result( + payload, + runtime_root=context.runtime_root, + goal_id=args.goal_id, + agent_id=args.agent_id, + todo_id=rollout_todo_id, + turn_instance_id=spend_turn_instance_id, + replan_obligation_id=rollout_replan_obligation_id, + goal_ref=goal_ref, + ) + attach_reward_memory_ingest_after_spend( + payload, + execute=bool(args.execute), + runtime_root=context.runtime_root, + registry_path=registry_path, + goal_id=args.goal_id, + agent_id=args.agent_id, + todo_id=rollout_todo_id, + turn_instance_id=spend_turn_instance_id, + replan_obligation_id=rollout_replan_obligation_id, + goal_ref=goal_ref, + ) def handle_quota_command( @@ -504,6 +606,9 @@ def handle_quota_command( append_cli_rollout_event: RolloutEventAppender, capture_directory: Path | None = None, ) -> int: + native_result = handle_native_context_command(args, print_payload=print_payload) + if native_result is not None: + return native_result usage_quota_started = time.time_ns() // 1_000_000 heartbeat_turn_id: str | None = None heartbeat_receipt_existing: dict[str, object] | None = None @@ -522,6 +627,9 @@ def handle_quota_command( getattr(args, "decision_output_dir", None) is not None or getattr(args, "decision_output_root", None) is not None or getattr(args, "decision_handoff_socket", None) is not None + or getattr(args, "codex_context_dir", None) is not None + or getattr(args, "codex_context_root", None) is not None + or getattr(args, "context_delivery", "off") != "off" ): validate_quota_command_context_request(args) capture_directory = prepare_decision_capture(args) @@ -915,56 +1023,14 @@ def handle_quota_command( ), ) else: - append_cli_rollout_event( - payload, - registry_path=registry_path, - runtime_root_arg=runtime_root_arg, - event_kind=QUOTA_EVENT_KINDS[args.quota_command], - goal_ref=goal_ref, - agent_id=args.agent_id, - todo_id=rollout_todo_id, - run_id=( - spend_turn_instance_id - if args.quota_command == "spend-slot" - else None - ), - status=str( - payload.get("effective_action") - or payload.get("decision") - or payload.get("mode") - or args.quota_command - ), - summary=( - f"quota {args.quota_command} decision=" - f"{payload.get('decision') or payload.get('mode')} " - f"state={payload.get('state') or ''}" - ), - details=rollout_details, - allow_failed=args.quota_command == "should-run", + _record_non_guard_quota_event( + payload, args, registry_path=registry_path, + runtime_root_arg=runtime_root_arg, context=context, goal_ref=goal_ref, + rollout_todo_id=rollout_todo_id, + rollout_replan_obligation_id=rollout_replan_obligation_id, + spend_turn_instance_id=spend_turn_instance_id, rollout_details=rollout_details, + append_cli_rollout_event=append_cli_rollout_event, ) - if args.quota_command == "spend-slot" and spend_turn_instance_id: - attach_spend_settlement_result( - payload, - runtime_root=runtime_root, - goal_id=args.goal_id, - agent_id=args.agent_id, - todo_id=rollout_todo_id, - turn_instance_id=spend_turn_instance_id, - replan_obligation_id=rollout_replan_obligation_id, - goal_ref=goal_ref, - ) - attach_reward_memory_ingest_after_spend( - payload, - execute=bool(args.execute), - runtime_root=runtime_root, - registry_path=registry_path, - goal_id=args.goal_id, - agent_id=args.agent_id, - todo_id=rollout_todo_id, - turn_instance_id=spend_turn_instance_id, - replan_obligation_id=rollout_replan_obligation_id, - goal_ref=goal_ref, - ) attach_reward_memory_recall_after_should_run( payload, quota_command=args.quota_command, diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index f19c3f03a4..42c6d99923 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -5,19 +5,198 @@ import hashlib import json import os +import re import shlex import socket +import stat import tempfile from pathlib import Path +from typing import Any +from collections.abc import Callable from ..control_plane.quota.error_codes import QuotaCommandValidationError, QuotaDecisionTransportError -from ..control_plane.turn_driver.decision_capture import validate_handoff_socket +from ..control_plane.turn_driver.decision_capture import ( + validate_handoff_socket, CONTEXT_CAPTURE_READ_GUIDANCE, context_capture_read_command, +) + + +def native_context_activation(args: argparse.Namespace) -> dict[str, Any]: + from ..control_plane.turn_driver.codex_context import context_delivery_activation + + fields = {key: getattr(args, key, None) for key in ("host_surface", "scheduler_owner", "execution_mode")} + profiles = [value for value in (getattr(args, "runtime_profile", None), + "codex_app_heartbeat" if getattr(args, "codex_app", False) else None, + "trae_app" if getattr(args, "trae_app", False) else None) if value is not None] + try: + if len(profiles) > 1: + return context_delivery_activation(getattr(args, "context_delivery", "off")) + return context_delivery_activation(getattr(args, "context_delivery", "off"), + runtime_profile=profiles[0] if profiles else None, + scheduler_execution_context=fields if any(fields.values()) else None, + benchmark_mode=os.environ.get("LOOPX_EXECUTION_MODE") + if os.environ.get("LOOPX_CONTEXT_DELIVERY") == "on" else None) + except ValueError as exc: + 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 re.fullmatch(r"[0-9a-f]{64}", 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. + + Verify the complete byte digest on every page. The cursor addresses bytes + in that exact capture, so a replacement cannot silently mix two decisions. + EOF describes this page, never a model's complete-consumption receipt. + """ + try: + digest = getattr(args, "capture_sha256", None) + value = getattr(args, "capture_file", None) + offset, size = args.offset, args.page_bytes + from ..control_plane.turn_driver.codex_context import codex_context_directory + + context_dir = codex_context_directory(directory=getattr(args, "codex_context_dir", None), + root=getattr(args, "codex_context_root", None)) + if context_dir: + if value or digest or offset != 0: + raise ValueError("session lookup starts at offset zero without capture-file or capture-sha256; follow next_command afterwards") + from ..control_plane.turn_driver.codex_context import read_codex_context_reference + + value, digest = read_codex_context_reference(context_dir, + goal_id=getattr(args, "goal_id", None), agent_id=getattr(args, "agent_id", None)) + if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", 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") + 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_READ_GUIDANCE + if end < len(raw): + result["next_command"] = context_capture_read_command(path, digest, offset=end, page_bytes=size) + return result + 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. + return {"ok": False, "schema_version": "context_capture_page_v1", + "reason": str(exc) if isinstance(exc, ValueError) else "original capture could not be read", + "recovery": "Restore the original digest-bound capture; no guard, admission or spend was performed."} + + +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. Any optional transport failure + returns the original full read reference, never a replacement guard. + Invalid run-mode opt-ins fail before that optional transport path. + """ + try: + activation = native_context_activation(args) + path, raw, payload = _read_capture(args.capture_file, args.capture_sha256) + identity = payload.get("agent_identity") + receipt = payload.get("heartbeat_receipt") + if (payload.get("ok") is not True or not isinstance(identity, dict) or not isinstance(receipt, dict) + or not args.goal_id or not args.agent_id or not args.turn_instance_id + or payload.get("goal_id") != args.goal_id or identity.get("agent_id") != args.agent_id + or receipt.get("turn_instance_id") != args.turn_instance_id): + raise ValueError("context-project requires the captured quota decision's exact Goal, Agent and Turn") + if args.turn_envelope or args.decision_output_dir or args.decision_handoff_socket: + raise ValueError("context-project presents the saved quota view; it does not change view or handoff") + except QuotaCommandValidationError as exc: + return {"ok": False, "reason": str(exc)} + except (OSError, ValueError, TypeError, RecursionError) as exc: + return {"ok": False, "reason": str(exc) if isinstance(exc, ValueError) else "original capture could not be read", + "recovery": "Restore the original digest-bound decision; no guard, admission or spend was performed."} + + reference = {"path": str(path), "sha256": args.capture_sha256, + "command": context_capture_read_command(path, args.capture_sha256)} + full_reference = dict(reference) + mode = "full" + target = 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") + root = Path(args.decision_output_root).expanduser().absolute() + metadata = root.lstat() + if root.resolve(strict=True) != root or not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.getuid() or metadata.st_mode & 0o077: + raise ValueError("saved projection needs an existing private physical capture root") + directory = codex_context_directory(directory=args.codex_context_dir, + root=args.codex_context_root, create=True) + if not directory: + raise ValueError("native context is unavailable") + 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()) + 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")} + except (OSError, ValueError, TypeError, RecursionError): + pass # Optional presentation unavailable: the original full reference is already verified. + return {"ok": True, "mode": mode, **reference, **({"target": target} if target else {}), + "recovery": full_reference, + "capture_sha256": args.capture_sha256, + **({"context_delivery": activation} if activation["enabled"] else {}), + "instruction": "Presentation of the original decision only; authority and freshness are unchanged. " + + CONTEXT_CAPTURE_READ_GUIDANCE} def prepare_decision_capture(args: argparse.Namespace) -> Path | None: + enabled = native_context_activation(args)["enabled"] + if enabled and not (getattr(args, "codex_context_dir", None) or getattr(args, "codex_context_root", None) + or getattr(args, "decision_handoff_socket", None)): + raise QuotaCommandValidationError("context-delivery on requires a native context location or explicit host handoff") requested = getattr(args, "decision_output_dir", None) root = getattr(args, "decision_output_root", None) handoff = getattr(args, "decision_handoff_socket", None) + if enabled and (getattr(args, "codex_context_dir", None) is not None + or getattr(args, "codex_context_root", None) is not None) and ( + args.format != "json" or (requested is None and root is None)): + raise QuotaCommandValidationError( + "native Codex context delivery needs JSON and a capture destination") if handoff is not None: validate_handoff_socket(handoff) if requested is None and root is None: @@ -40,6 +219,7 @@ def prepare_decision_capture(args: argparse.Namespace) -> Path | None: if parent.is_symlink() or not parent.is_dir(): raise OSError("capture root must be an existing directory, not a symlink") return Path(tempfile.mkdtemp(prefix="decision-", dir=parent)) + assert requested is not None # root is absent; exactly one destination was checked above. directory = Path(requested).expanduser().absolute() # A new directory per invocation prevents overwriting a prior observation, # including a prior invocation in the same Turn. Existing symlinks fail too. @@ -67,7 +247,7 @@ def capture_decision(directory: Path | None, payload: dict[str, object]) -> None ) from exc -def bind_capture_selection_transport(payload: dict, args: argparse.Namespace) -> None: +def bind_capture_selection_transport(payload: dict[str, Any], args: argparse.Namespace) -> None: """Retain caller display/capture options in the existing selection command. Only transport argv is added here; the typed quota owner still selects the @@ -89,10 +269,17 @@ def bind_capture_selection_transport(payload: dict, args: argparse.Namespace) -> suffix += " --decision-handoff-socket " + shlex.quote(args.decision_handoff_socket) if getattr(args, "turn_envelope", False): suffix += " --turn-envelope" + if getattr(args, "context_delivery", "off") != "off": + suffix += " --context-delivery " + shlex.quote(args.context_delivery) + if getattr(args, "codex_context_dir", None): + suffix += " --codex-context-dir " + shlex.quote(args.codex_context_dir) + if getattr(args, "codex_context_root", None): + suffix += " --codex-context-root " + shlex.quote(args.codex_context_root) selection["command_args_template"] = template + suffix -def handoff_decision(directory: Path | None, socket_path: str | None) -> None: +def handoff_decision(directory: Path | None, socket_path: str | None, + context_delivery_enabled: bool = False) -> tuple[dict[str, Any], str] | None: """Withhold the original output until the Host releases these exact bytes. No second guard, admission, settlement or retry is performed here. A lost @@ -100,7 +287,7 @@ def handoff_decision(directory: Path | None, socket_path: str | None) -> None: recovery; it must never be silently replaced by a later observation. """ if socket_path is None: - return + return None if directory is None: raise ValueError("decision handoff requires a captured decision") path = directory / "decision.json" @@ -114,7 +301,8 @@ def handoff_decision(directory: Path | None, socket_path: str | None) -> None: with channel.makefile("rb") as stream: line = stream.readline(1025) response = json.loads(line) - if response != {"released": True, "sha256": digest}: + ordinary_release = {"released": True, "sha256": digest} + if response not in (ordinary_release, {**ordinary_release, "context_delivery": True}): raise ValueError("Host did not release the original decision digest") if path.read_bytes() != raw: raise ValueError("captured decision changed during handoff") @@ -123,3 +311,95 @@ def handoff_decision(directory: Path | None, socket_path: str | None) -> None: "Decision handoff failed after guard evaluation; inspect the original " f"Turn receipt and {path}. Do not blindly rerun the guard." ) from exc + if response.get("context_delivery") is not True or not context_delivery_enabled: + return None + # Both the supported caller opt-in and explicit Host request are required. + # The release itself does not prove + # delivery/retention. Read one private snapshot, never an arbitrary pathname + # supplied by a response or a model. Invalid optimization input selects full. + request_path = directory / "delivery-request.json" + try: + from ..control_plane.effect_runtime import MAX_REQUEST_BYTES + descriptor = os.open(request_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, "rb") as source: + metadata = os.fstat(source.fileno()) + if (not stat.S_ISREG(metadata.st_mode) or metadata.st_mode & 0o077 or + metadata.st_size > MAX_REQUEST_BYTES or metadata.st_uid != os.getuid()): + return {}, digest + raw = source.read(MAX_REQUEST_BYTES + 1) + if len(raw) > MAX_REQUEST_BYTES: + return {}, digest + request = json.loads(raw) + return (request if isinstance(request, dict) else {}), digest + except (OSError, ValueError, RecursionError): + return {}, digest + + +def handle_native_context_command(args: argparse.Namespace, *, print_payload: Callable[..., None]) -> int | None: + """Dispatch native presentation/lifecycle IO before any quota authority read.""" + if args.quota_command in ("context-config", "context-hook"): + try: + activation = native_context_activation(args) + except QuotaCommandValidationError as exc: + payload = {"ok": False, "reason": str(exc)} + print_payload(payload, args.format, lambda value: json.dumps(value, ensure_ascii=False, indent=2)) + return 1 + if args.quota_command == "context-config": + from ..command_invocation import loopx_cli_argv + from ..control_plane.turn_driver.codex_context import codex_context_configuration + + try: + if not activation["enabled"]: + payload = {"ok": True, "context_delivery": activation} + print_payload(payload, args.format, lambda value: json.dumps(value, ensure_ascii=False, indent=2)) + return 0 + directory = getattr(args, "codex_context_root", None) or args.codex_context_dir + if not directory: + raise ValueError("context-config requires codex-context-root or codex-context-dir") + config = codex_context_configuration(directory, goal_id=args.goal_id, agent_id=args.agent_id, + cli_argv=loopx_cli_argv(), scenario=activation["scenario"], session_root=bool(getattr(args, "codex_context_root", None))) + payload = {"ok": True, "context_delivery": activation, "native_config": config, + "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. " + "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. " + "This command installs nothing and grants no execution authority."} + except (OSError, ValueError, TypeError) as exc: + payload = {"ok": False, "reason": str(exc)} + print_payload(payload, args.format, lambda value: json.dumps(value, ensure_ascii=False, indent=2)) + return 0 if payload["ok"] else 1 + 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 + + # Native transport only: no registry discovery, guard or quota effects. + try: + if not activation["enabled"]: + print("{}") + return 0 + 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")): + 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) + else: + payload = {} + except (OSError, ValueError, TypeError, RecursionError): + payload = {} + print(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))) + return 0 + if args.quota_command == "context-project": + payload = project_saved_context_capture(args) + print_payload(payload, args.format, lambda value: str(value.get("command", value.get("reason", "")))) + return 0 if payload["ok"] else 1 + if args.quota_command == "context-read": + # Presentation recovery reads the original capture, never current Goal + # state. In particular it must not evaluate a guard or append a receipt. + payload = read_context_capture(args) + print_payload(payload, args.format, lambda value: str(value.get("content", value.get("reason", "")))) + return 0 if payload["ok"] else 1 + return None diff --git a/loopx/cli_commands/quota_registration.py b/loopx/cli_commands/quota_registration.py index 7eed4e01a2..80dd08697f 100644 --- a/loopx/cli_commands/quota_registration.py +++ b/loopx/cli_commands/quota_registration.py @@ -27,6 +27,10 @@ def register_quota_command( "status", "plan", "should-run", + "context-read", + "context-project", + "context-hook", + "context-config", "monitor-poll", "scheduler-ack", "scheduler-ack-current", @@ -35,7 +39,7 @@ def register_quota_command( "void-slot", ], default="status", - help="Use status for all groups, plan for next-turn groups, should-run for one goal, monitor-poll for no-spend quiet poll evidence, scheduler-ack for successful Codex App RRULE state, scheduler-fail-current to suppress a repeated failed host update pair, spend-slot for accounting, or void-slot for a non-destructive accounting correction.", + help="Use status for all groups, plan for next-turn groups, should-run for one goal, context-read for bounded original-capture recovery without a guard, context-project to present an existing digest-bound quota capture with native full/delta and a full-read fallback, context-hook for opt-in native Codex lifecycle IO, context-config for native hook/recovery configuration without installation or trust changes, monitor-poll for no-spend quiet poll evidence, scheduler-ack for successful Codex App RRULE state, scheduler-fail-current to suppress a repeated failed host update pair, spend-slot for accounting, or void-slot for a non-destructive accounting correction.", ) quota_parser.add_argument( "--goal-id", @@ -196,6 +200,28 @@ def register_quota_command( "Requires a capture destination and a same-user Unix socket in a private directory. " "Waits at most 120s for a digest-bound release; failure retains the original receipt."), ) + quota_parser.add_argument("--capture-file", help="For context-read/context-project: original captured JSON file; no guard is evaluated.") + from ..control_plane.turn_driver.codex_context import CONTEXT_DELIVERY_SETTINGS + + quota_parser.add_argument("--context-delivery", choices=CONTEXT_DELIVERY_SETTINGS, default="off", + help="Experimental native packet reuse; off by default. On uses existing host declarations: " + "Codex App heartbeat/resume/recovery (--codex-app), or the Codex benchmark heartbeat worker " + "(generic_cli plus its explicit heartbeat worker settings). On rejects missing, contradictory or unsupported modes. " + "A directory alone never enables it; declarations are not retention proof.") + context_location = quota_parser.add_mutually_exclusive_group() + context_location.add_argument("--codex-context-dir", + help="Opt-in private native Codex lifecycle directory. For context-hook, consume one native hook event on stdin. " + "For should-run, requires JSON and a decision capture destination; unknown retention stays full. " + "For context-read without a file/hash, select the current native session/workspace capture for --goal-id/--agent-id; " + "start at offset zero, then follow digest-pinned next_command. Does not install hooks, enable tracing, or change native trust.") + context_location.add_argument("--codex-context-root", + help="Existing private native context root, with independent journals per native session. " + "Use for shared Codex App hook configuration; cannot combine with --codex-context-dir. " + "context-config renders native configuration for the exact Goal/Agent; no files, trust, guard or spend are changed.") + 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).") quota_parser.add_argument( "--turn-instance-id", help=( diff --git a/loopx/cli_commands/support_control.py b/loopx/cli_commands/support_control.py index 0cc9e4e493..e8f3a33f2f 100644 --- a/loopx/cli_commands/support_control.py +++ b/loopx/cli_commands/support_control.py @@ -407,6 +407,9 @@ def handle_support_control_command( turn_instance_id=args.turn_instance_id, decision_output_root=args.decision_output_root, decision_handoff_socket=args.decision_handoff_socket, + codex_context_dir=args.codex_context_dir, + codex_context_root=args.codex_context_root, + context_delivery=args.context_delivery, turn_envelope=args.turn_envelope, reward_memory_enabled=reward_memory_enabled, ) diff --git a/loopx/cli_commands/support_control_heartbeat_registration.py b/loopx/cli_commands/support_control_heartbeat_registration.py index 0c9e506745..7e9dc6cb57 100644 --- a/loopx/cli_commands/support_control_heartbeat_registration.py +++ b/loopx/cli_commands/support_control_heartbeat_registration.py @@ -159,7 +159,8 @@ def register_heartbeat_control_commands( heartbeat_prompt_parser.add_argument( "--decision-output-root", - help="Save full local decisions without changing the display; requires thin mode and an explicit host-owned Turn ID.", + help="Save full local decisions without changing the display; requires thin mode and an explicit host-owned Turn ID, " + "or codex-context-root with the normal per-wake LOOPX_TURN binding.", ) heartbeat_prompt_parser.add_argument( @@ -173,6 +174,24 @@ def register_heartbeat_control_commands( help="Retain the explicit private Host handoff on generated guards; requires --decision-output-root.", ) + from ..control_plane.turn_driver.codex_context import CONTEXT_DELIVERY_SETTINGS + + heartbeat_prompt_parser.add_argument("--context-delivery", choices=CONTEXT_DELIVERY_SETTINGS, default="off", + help="Experimental native packet reuse; off by default. On uses existing host declarations: " + "Codex App heartbeat/resume/recovery (--codex-app), or the Codex benchmark heartbeat worker " + "(generic_cli plus its explicit heartbeat worker settings). On rejects missing, contradictory or unsupported modes. " + "A directory alone never enables it; declarations are not retention proof.") + context_location = heartbeat_prompt_parser.add_mutually_exclusive_group() + context_location.add_argument( + "--codex-context-dir", + help="Carry the opt-in native context directory into guards; requires --decision-output-root. Does not install hooks or enable tracing.", + ) + context_location.add_argument( + "--codex-context-root", + help="Carry a shared private native context root into guards, isolated per native session; " + "requires --decision-output-root. Use quota context-config to render native setup.", + ) + heartbeat_prequota_parser = subparsers.add_parser( "heartbeat-prequota", help=( diff --git a/loopx/control_plane/capabilities/explore_turn_context.ts b/loopx/control_plane/capabilities/explore_turn_context.ts index 1c50cbf1be..4d864d2ebf 100644 --- a/loopx/control_plane/capabilities/explore_turn_context.ts +++ b/loopx/control_plane/capabilities/explore_turn_context.ts @@ -10,6 +10,23 @@ import {EXPLORE_WRITEBACK_SUMMARY_LIMIT, exploreResultWritebackAffordance} from import {EXPLORE_RESULT_PAGE_MAX, normalizeExploreResultPages} from "./explore_result_page.ts"; import {projectExploreMeasurementFeedback} from "./explore_measurements.ts"; +/** Reusable evidence and instructions in this owner's read model. Current + * selection, eligibility, source revisions and pagination stay outside this + * split. Exact array edits retain every change, including its status, + * provenance and applicability; no timestamp or revision is normalized away. */ +export function exploreReusableContextPaths(): string[][] { + return [ + ...["recent_nodes", "recent_findings", "writeback_results", "measurement_feedback", + "path_delta_attachment_template", "linked_question_attachment_template", + "optional_question_evolution_example", "result_writeback_guidance", + "result_attachment_template", "optional_measurement_example", "measurement_guidance", + "record_node_template", "record_finding_template", "guidance", + "result_writeback_option", "result_writeback_inline_option", "result_writeback_inline_field", + "result_attachment_schema", "path_delta_attachment_schema"].map(key => ["graph", key]), + ["harness", "guidance"], + ]; +} + function rows(value: unknown): JsonObject[] { return Array.isArray(value) ? value.map(item => requireJsonObject(item, "Explore row")) : []; } diff --git a/loopx/control_plane/capability_hooks.ts b/loopx/control_plane/capability_hooks.ts index 14deda4e7a..9fb75e3457 100644 --- a/loopx/control_plane/capability_hooks.ts +++ b/loopx/control_plane/capability_hooks.ts @@ -21,6 +21,15 @@ import { projectRepositoryDeliveryGate } from "./work_items/repository_delivery. import { projectPendingCapabilityIntent } from "./work_items/pending_capability_intent.ts"; import {exploreResultWritebackAffordance} from "./capabilities/explore_result_writeback.ts"; +import {exploreReusableContextPaths} from "./capabilities/explore_turn_context.ts"; + +/** The registered owner selects reusable read-model subtrees. Unknown hooks + * remain complete; payloads cannot nominate their own execution fields. */ +export function reusableTurnContextPaths(source: JsonObject): string[][] { + return source.source === "turn_start_capability_hook" && source.hook_id === "explore.turn_context" + && source.capability_id === "explore" && source.kind === "explore_turn_context" + ? exploreReusableContextPaths() : []; +} export const CAPABILITY_HOOK_REGISTRATION_SCHEMA_VERSION = CAPABILITY_HOOK_REGISTRATION_SCHEMA; diff --git a/loopx/control_plane/heartbeat/bootstrap_prompt.py b/loopx/control_plane/heartbeat/bootstrap_prompt.py index 7429da1e3d..a633d02c7f 100644 --- a/loopx/control_plane/heartbeat/bootstrap_prompt.py +++ b/loopx/control_plane/heartbeat/bootstrap_prompt.py @@ -66,9 +66,12 @@ def _goal_command(args, *, registry: Path, legacy: bool) -> list[str]: ("runtime_profile", "--runtime-profile"), ("visible_goal_host", "--visible-goal-host"), ("host_surface", "--host-surface"), ("scheduler_owner", "--scheduler-owner"), ("execution_mode", "--execution-mode"), + ("decision_output_root", "--decision-output-root"), + ("codex_context_root", "--codex-context-root"), + ("context_delivery", "--context-delivery"), ): value = getattr(args, field, None) - if value is not None: + if value is not None and not (field == "context_delivery" and value == "off"): if field == "active_state": value = str(Path(value).expanduser().resolve()) command += [flag, value] @@ -78,6 +81,8 @@ def _goal_command(args, *, registry: Path, legacy: bool) -> list[str]: command += [flag, value] if args.cli_bin != "loopx": command += ["--cli-bin", args.cli_bin] + if getattr(args, "turn_envelope", False): + command.append("--turn-envelope") if legacy: if args.codex_app: command.append("--codex-app") @@ -112,17 +117,19 @@ def host_bootstrap_binding(prompt: str) -> dict | None: return None values = dict(cli_bin=command[0], turn_instance_id=None, codex_app=False, trae_app=False, - full=False, compact=False, brief=False, thin=False, + full=False, compact=False, brief=False, thin=False, turn_envelope=False, visible_goal_host=None, available_capabilities=[], agent_scopes=[], runtime_root=None) singles = {"--" + name.replace("_", "-"): name for name in ( "registry", "runtime_root", "goal_id", "agent_id", "active_state", "material_rule", "permission_rule", "runtime_profile", "visible_goal_host", - "host_surface", "scheduler_owner", "execution_mode", "cli_bin")} + "host_surface", "scheduler_owner", "execution_mode", "cli_bin", + "decision_output_root", "codex_context_root", "context_delivery")} repeated = {"--agent-scope": "agent_scopes", "--available-capability": "available_capabilities"} booleans = { "--codex-app": "codex_app", "--trae_app": "trae_app", + "--turn-envelope": "turn_envelope", **{"--" + name.replace("_", "-"): name for name in ("full", "compact", "brief", "thin")}, } index = 3 diff --git a/loopx/control_plane/heartbeat/builder.py b/loopx/control_plane/heartbeat/builder.py index 4e1845953c..36425f1df2 100644 --- a/loopx/control_plane/heartbeat/builder.py +++ b/loopx/control_plane/heartbeat/builder.py @@ -176,6 +176,9 @@ def _heartbeat_prompt_commands( turn_identity_arg: str, decision_output_root: Path | None, decision_handoff_socket: str | None, + codex_context_dir: str | None, + codex_context_root: str | None, + context_delivery: str, turn_envelope: bool, ) -> dict[str, str | None]: quota_guard_command = render_quota_guard_command( @@ -280,6 +283,12 @@ def _heartbeat_prompt_commands( capture_arg = " --decision-output-root " + shlex.quote(str(decision_output_root)) if decision_handoff_socket is not None: capture_arg += " --decision-handoff-socket " + shlex.quote(decision_handoff_socket) + if context_delivery != "off": + capture_arg += " --context-delivery " + shlex.quote(context_delivery) + if codex_context_dir is not None: + capture_arg += " --codex-context-dir " + shlex.quote(codex_context_dir) + if codex_context_root is not None: + capture_arg += " --codex-context-root " + shlex.quote(codex_context_root) display_arg = " --turn-envelope" if turn_envelope else "" for key in ("quota_guard_command", "task_body_quota_guard_command"): commands[key] = str(commands[key]) + display_arg + capture_arg @@ -315,6 +324,9 @@ def build_heartbeat_prompt( reward_memory_enabled: bool = True, decision_output_root: str | Path | None = None, decision_handoff_socket: str | None = None, + codex_context_dir: str | None = None, + codex_context_root: str | None = None, + context_delivery: str = "off", turn_envelope: bool = False, ) -> dict[str, Any]: if not (full or compact or brief or thin): @@ -373,12 +385,26 @@ def build_heartbeat_prompt( runtime_profile=runtime_profile, scheduler_execution_context=scheduler_execution_context, ) + from ..turn_driver.codex_context import context_delivery_activation + + import os + + activation = context_delivery_activation(context_delivery, runtime_profile=runtime_profile, + scheduler_execution_context=scheduler_execution_context, + benchmark_mode=os.environ.get("LOOPX_EXECUTION_MODE") + if os.environ.get("LOOPX_CONTEXT_DELIVERY") == "on" else None) + if not activation["enabled"]: + codex_context_dir = codex_context_root = None + effective_context_delivery = "on" if activation["enabled"] else "off" decision_output_root = resolve_heartbeat_capture_root( decision_output_root, thin=thin, full=full, compact=compact, brief=brief, native_goal_host=native_goal_host, turn_instance_id=normalized_turn_instance_id, decision_handoff_socket=decision_handoff_socket, + codex_context_dir=codex_context_dir, + codex_context_root=codex_context_root, + context_delivery=effective_context_delivery, turn_envelope=turn_envelope, ) explicit_agent_scopes = normalize_agent_scopes(agent_scopes) @@ -468,6 +494,9 @@ def build_heartbeat_prompt( turn_identity_arg=turn_identity_arg, decision_output_root=decision_output_root, decision_handoff_socket=decision_handoff_socket, + codex_context_dir=codex_context_dir, + codex_context_root=codex_context_root, + context_delivery=effective_context_delivery, turn_envelope=turn_envelope, ) cli_preflight = render_cli_preflight(cli_bin=cli_bin) @@ -546,6 +575,7 @@ def build_heartbeat_prompt( "agent_profile": agent_profile_prompt_projection(agent_profile), "registered_agents": normalized_registered_agents, "runtime_profile": runtime_profile, + **({"context_delivery": activation} if context_delivery == "on" else {}), "reward_memory_enabled": reward_memory_enabled, "scheduler_execution_context": scheduler_execution_context, **( diff --git a/loopx/control_plane/heartbeat/host.py b/loopx/control_plane/heartbeat/host.py index e930e9c4cf..e7544a736c 100644 --- a/loopx/control_plane/heartbeat/host.py +++ b/loopx/control_plane/heartbeat/host.py @@ -30,16 +30,27 @@ def resolve_heartbeat_capture_root( native_goal_host: bool, turn_instance_id: str | None, decision_handoff_socket: str | None = None, + codex_context_dir: str | None = None, + codex_context_root: str | None = None, + context_delivery: str = "off", turn_envelope: bool = False, ) -> Path | None: """Validate capture transport for an already resolved host-owned Turn.""" + enabled = context_delivery == "on" + if enabled and not (codex_context_dir or codex_context_root): + raise ValueError("context-delivery requires a native context location") + if codex_context_dir is not None and codex_context_root is not None: + raise ValueError("choose codex-context-dir or codex-context-root") + if (codex_context_dir is not None or codex_context_root is not None) and root is None: + raise ValueError("native context delivery requires a capture root") if root is None: if decision_handoff_socket is not None: raise ValueError("decision handoff requires an explicit decision 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 or not turn_instance_id: + if (not thin or full or compact or brief or native_goal_host + 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() if directory.is_symlink() or not directory.is_dir(): diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts new file mode 100644 index 0000000000..6ee18026bc --- /dev/null +++ b/loopx/control_plane/quota/context_delivery.ts @@ -0,0 +1,372 @@ +/** 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. */ +import type {JsonObject} from "../effect_program.ts"; +import {jsonObject, requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; +import {canonicalAuthorityJson, canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts"; +import {reusableTurnContextPaths} from "../capability_hooks.ts"; + +const VERSION = "context_delivery_v1"; +const MAX_ANCESTRY = 32; +type Path = (string | number)[]; +type Split = {fresh: JsonObject; valid: boolean}; +const clone = (value: T): T => structuredClone(value); +const equal = (a: unknown, b: unknown): boolean => { + // Identical requirement strings need no canonical JSON allocation. Keep the + // existing type-sensitive comparison for every other value. + if (typeof a === "string" && a === b) return true; + return a === undefined || b === undefined ? a === b + : JSON.stringify(canonicalAuthorityJson(a)) === JSON.stringify(canonicalAuthorityJson(b)); +}; +const digest = (value: unknown): string => `sha256:${canonicalAuthoritySha256(value)}`; +const bytes = (value: unknown): number => Buffer.byteLength(JSON.stringify(value), "utf8"); + +/** A title equal to the complete body carries no second instruction. Never use + * the bounded hot title/text as the source, and retain a genuinely distinct title. */ +export function compactSelectedTodoSource(content: JsonObject): JsonObject { + const todo = jsonObject(content.todo); + if (!todo || typeof todo.text !== "string" || todo.title !== todo.text) return content; + const copy = {...todo}; + delete copy.title; + return {...content, todo: copy}; +} + +function parentAt(root: JsonObject, path: Path): Record { + let value: unknown = root; + for (const key of path.slice(0, -1)) value = (value as Record)[key]; + return value as Record; +} + +/** Registered requirement fields and capability evidence subtrees are reusable. Revisions, source + * failures, claims, reviews, grants, current gates and unknown fields stay fresh. + * Source-array order is transport layout, never the identity of a requirement. */ +function splitContext(context: JsonObject): Split { + const fresh = clone(context), identities = new Set(); + let valid = context.complete === true; + const add = (identity: string, path: Path, structured = false) => { + if (identities.has(identity)) { valid = false; return; } + identities.add(identity); + const parent = parentAt(fresh, path), key = path[path.length - 1]; + if (!Object.hasOwn(parent, key)) return; + // These adapters own literal text, not arbitrary nested control records. + if (!structured && parent[key] !== null && typeof parent[key] !== "string") { valid = false; return; } + delete parent[key]; + }; + add("work_context_instruction", ["instruction"]); + const userRead = jsonObject(jsonObject(jsonObject(fresh.user_todos)?.authority_read)?.todo_read_model); + if (Array.isArray(userRead?.contract_fields) && userRead.contract_fields.every(field => typeof field === "string")) + add("user_todo_contract_fields", ["user_todos", "authority_read", "todo_read_model", "contract_fields"], true); + const sources = Array.isArray(fresh.sources) ? fresh.sources : []; + const sourceIds = new Set(); + for (let i = 0; i < sources.length; i++) { + const source = jsonObject(sources[i]), content = jsonObject(source?.content); + if (!source || !content || content.ok === false || content.read_complete === false) continue; + const prefix: Path = ["sources", i, "content"]; + const kind = source.source; + const known = ["goal_state", "selected_todo", "goal_acceptance"].includes(String(kind)) || reusableTurnContextPaths(source).length > 0; + if (known && typeof source.reason === "string") add(`source_reason:${i}`, ["sources", i, "reason"]); + const readModel = jsonObject(jsonObject(content.authority_read)?.todo_read_model); + if (kind === "selected_todo" && Array.isArray(readModel?.contract_fields) + && readModel.contract_fields.every(field => typeof field === "string")) + add(`todo_contract_fields:${i}`, [...prefix, "authority_read", "todo_read_model", "contract_fields"], true); + if (kind === "goal_state" && typeof content.state_file === "string" + && typeof content.text === "string" && Array.isArray(content.excluded_todo_regions)) { + const id = JSON.stringify([kind, content.state_file]); + if (sourceIds.has(id)) { valid = false; continue; } + sourceIds.add(id); + add(id, [...prefix, "text"]); + } else if (kind === "selected_todo") { + const todo = jsonObject(content.todo); + // An already referenced selected body lacks an independent source id. + // Keep such residual metadata full rather than guessing its identity. + if (!todo || typeof todo.todo_id !== "string" || !todo.todo_id) continue; + const id = JSON.stringify([kind, todo.todo_id]); + if (sourceIds.has(id)) { valid = false; continue; } + sourceIds.add(id); + for (const key of ["text", "note", "title"]) add(JSON.stringify([kind, todo.todo_id, key]), [...prefix, "todo", key]); + } else if (kind === "goal_acceptance") { + if (sourceIds.has(kind)) { valid = false; continue; } + sourceIds.add(kind); + const contract = jsonObject(content.goal_acceptance_contract); + const outcomes = jsonObject(contract?.outcome_requirements)?.outcomes; + if (!Array.isArray(outcomes)) continue; + const seen = new Set(); + for (let j = 0; j < outcomes.length; j++) { + const outcome = jsonObject(outcomes[j]); + if (!outcome || typeof outcome.id !== "string" || !outcome.id || seen.has(outcome.id)) { + valid = false; continue; + } + seen.add(outcome.id); + const path = [...prefix, "goal_acceptance_contract", "outcome_requirements", "outcomes", j]; + add(JSON.stringify([kind, outcome.id, "objective"]), [...path, "objective"]); + const requirements = outcome.requirements; + if (!Array.isArray(requirements)) continue; + const requirementIds = new Set(); + for (let k = 0; k < requirements.length; k++) { + const requirement = jsonObject(requirements[k]); + if (!requirement || typeof requirement.id !== "string" || !requirement.id || requirementIds.has(requirement.id)) { + valid = false; continue; + } + requirementIds.add(requirement.id); + add(JSON.stringify([kind, outcome.id, requirement.id, "description"]), [...path, "requirements", k, "description"]); + } + } + } else { + const paths = reusableTurnContextPaths(source); + if (!paths.length) continue; + if (content.ok !== true || typeof content.goal_id !== "string" || typeof content.agent_id !== "string") continue; + const identity = [kind, source.hook_id, source.capability_id, content.goal_id, content.agent_id, content.todo_id ?? null]; + const id = JSON.stringify(identity); + if (sourceIds.has(id)) { valid = false; continue; } + sourceIds.add(id); + for (const path of paths) { + let value: unknown = content; + for (const key of path.slice(0, -1)) value = jsonObject(value)?.[key]; + if (jsonObject(value)) add(JSON.stringify([...identity, ...path]), [...prefix, ...path], true); + } + } + } + return {fresh, valid}; +} + +/** The projection surface is a transport binding, never a new admission mode. + * 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; removed?: string[]} + | {items: Record; length?: number}; +const pathKey = (path: Path): string => JSON.stringify(path); + +// These are existing quota projections. Unknown root fields stay fresh. Within +// these complete, capture-bound projections, exact reuse does not change any +// obligation or make a historical source authoritative. +const REUSABLE_PACKET_FIELDS = new Set([ + "registry", "runtime_root", "source", "project_asset_source", "long_task_cadence_hint", + "plan_summary", "reason", "recommended_action", + "goal_frontier_projection", "todo_write_hint", "vision_continuation_audit", "agent_lane_next_action", + "user_todo_summary", "agent_todo_summary", "blocked_priority_fallback", + "autonomous_backlog_candidates", "autonomous_monitor_candidates", "next_action_projection_warning", + "completed_todo_archive_warning", "promotion_readiness_warning", "next_handoff_condition", "agent_command", + "automation_cadence", "automation_cadence_readback", "open_count", + "turn_start_capability_hook_dispatch", "rollout_event", "reward_memory_recall", "todo_summary_projection", + "planning_horizon", "autonomous_replan_obligation", "replan_action_packet", "bounded_research_frontier", + "action_portfolio", "autonomous_replan_decision", "active_state_next_action", +]); + +/** Current controls are forced into the change tree even when equal. This avoids + * duplicating them in both a fresh frame and a patch. Static authoring guidance + * remains binding via exact reuse; commands, conditions and identity stay fresh. */ +function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: readonly string[]): {paths: Path[]; valid: boolean} { + const paths: Path[] = []; + let valid = true; + const leaves = (value: unknown, path: Path) => { + if (Array.isArray(value) && value.length) value.forEach((v, i) => leaves(v, [...path, i])); + else if (jsonObject(value) && Object.keys(value as JsonObject).length) + Object.entries(value as JsonObject).forEach(([k, v]) => leaves(v, [...path, k])); + else paths.push(path); + }; + const work = (value: unknown, path: Path) => { + const source = jsonObject(value); + if (!source) { paths.push(path); return; } + const split = splitContext(source); valid &&= split.valid; + leaves(split.fresh, path); + }; + const vision = (value: unknown, path: Path) => { + const audit = jsonObject(value); + if (!audit) { paths.push(path); return; } + for (const [key, item] of Object.entries(audit)) { + if (key === "required_before_closeout") continue; // complete binding instructions in the exact patch + if (key !== "vision_gap_judge" || !jsonObject(item)) { paths.push([...path, key]); continue; } + for (const field of Object.keys(item as JsonObject)) + if (!["agent_judge_instruction", "evidence_read_instruction", "registry_read_instruction", "done_only_when", "continue_when"].includes(field)) + paths.push([...path, key, field]); + } + }; + const settlement = (value: unknown, path: Path) => { + const plan = jsonObject(value); + if (!plan) { paths.push(path); return; } + for (const [key, item] of Object.entries(plan)) { + if (key !== "ordered_steps" || !Array.isArray(item)) { paths.push([...path, key]); continue; } + item.forEach((step, i) => { + if (!jsonObject(step)) { paths.push([...path, key, i]); return; } + for (const [field, detail] of Object.entries(step as JsonObject)) { + if (["precondition", "vision_authoring", "guidance"].includes(field)) continue; + if (field === "optional_attachments" && Array.isArray(detail)) { + detail.forEach((attachment, j) => { + const item = jsonObject(attachment), prefix = [...path, key, i, field, j]; + if (!item) { paths.push(prefix); return; } + for (const property of Object.keys(item)) if (property !== "guidance") paths.push([...prefix, property]); + }); + } else paths.push([...path, key, i, field]); + } + }); + } + }; + if (surface === "work_context") work(context, []); + else if (surface === "turn_envelope") { + for (const [key, value] of Object.entries(context)) { + if (key === "work_context") work(value, [key]); + else if (key === "writeback") { + const writeback = jsonObject(value); + if (!writeback) { paths.push([key]); continue; } + for (const [field, item] of Object.entries(writeback)) { + if (field === "settlement_plan") settlement(item, [key, field]); + else paths.push([key, field]); + } + } else paths.push([key]); + } + } else { + for (const [key, value] of Object.entries(context)) { + if (key !== "interaction_contract") { + if (!REUSABLE_PACKET_FIELDS.has(key) && !legacyQuotaFields.includes(key)) paths.push([key]); + continue; + } + const contract = jsonObject(value); + if (!contract) { paths.push([key]); continue; } + for (const [channel, fields] of Object.entries(contract)) { + if (!["agent_channel", "cli_channel"].includes(channel) || !jsonObject(fields)) { + paths.push([key, channel]); continue; + } + for (const [field, item] of Object.entries(fields as JsonObject)) { + const path = [key, channel, field]; + if (channel === "agent_channel" && field === "work_context") work(item, path); + else if (channel === "cli_channel" && field === "settlement_plan") settlement(item, path); + else if (field === "vision_continuation_audit") vision(item, path); + else paths.push(path); + } + } + } + } + return {paths, valid}; +} + +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))); + 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; + let delta: Delta; + if (jsonObject(a) && 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)}; + if (child) changed.push([field, child]); + } + const removed = Object.keys(old).filter(field => !Object.hasOwn(next, field)); + delta = {changed: Object.fromEntries(changed), ...(removed.length ? {removed} : {})}; + } else if (Array.isArray(a) && Array.isArray(b)) { + const items: [string, Delta][] = []; + b.forEach((value, i) => { + const child = i < a.length ? visit(a[i], value, [...path, i]) : {value: clone(value)}; + if (child) items.push([String(i), child]); + }); + delta = {items: Object.fromEntries(items), ...(a.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. + return bytes(delta) < bytes({value: b}) ? delta : {value: clone(b)}; + }; + return visit(before, after, []); +} + +/** 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 { + if (delta === null) return clone(before); + if ("value" in delta) return clone(delta.value); + if ("items" in delta) { + if (!Array.isArray(before)) throw new Error("array baseline missing"); + const result = clone(before); + if (delta.length !== undefined) result.length = delta.length; + for (const [key, item] of Object.entries(delta.items)) result[Number(key)] = applyTree(before[Number(key)], item); + return result; + } + const source = requireJsonObject(before, "object baseline"), result = clone(source); + for (const key of delta.removed ?? []) delete result[key]; + 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), + enumerable: true, writable: true, configurable: true}); + } + return result; +} + +function targetBinding(request: JsonObject, context: JsonObject, surface: Surface): JsonObject { + const scope = requireJsonObject(request.scope, "context delivery scope"); + const binding: JsonObject = {projection_version: VERSION, surface}; + for (const key of ["goal_id", "agent_id", "session_id", "context_epoch"]) { + binding[key] = requireNonEmptyString(scope[key], `scope.${key}`); + } + 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"] + .every(key => base[key] === target[key]); +} +function surfaceOf(value: unknown): Surface { + if (!SURFACES.includes(value as Surface)) throw new Error("unsupported context delivery 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. */ +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 binding = jsonObject(baseline?.target), prior = jsonObject(baseline?.context); + if (!binding || !prior || !retained || retained.complete !== true + || !equal(retained.target, binding) || !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"); + const before = freshPaths(prior, surface, legacyQuotaFields), after = freshPaths(context, surface, legacyQuotaFields); + if (!before.valid || !after.valid) return full("source_not_reusable"); + 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. Changes contain complete values, object changes/removals or exact array-index edits. 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; +} + +/** Validate the exact change tree against the immutable CURRENT capture before + * applying it. Wire keys never choose freshness, scope or arbitrary write paths. + * Recovery neither reruns admission nor mutates Goal/quota state. */ +export function restoreContextDelivery(request: JsonObject, legacyQuotaFields: readonly string[] = []): JsonObject { + const current = requireJsonObject(request.current, "current full capture"); + const currentContext = requireJsonObject(current.context, "current full context"); + const target = requireJsonObject(current.target, "current capture binding"); + if (current.mode !== "full" || current.schema_version !== VERSION || target.projection_version !== VERSION || target.context_digest !== digest(currentContext)) { + throw new Error("current full capture is invalid"); + } + const fallback = (): JsonObject => ({...clone(current), reason: "automatic_full_recovery", ancestry: 0}); + try { + const surface = surfaceOf(target.surface); + const delivery = requireJsonObject(request.delivery, "delivery"), baseline = requireJsonObject(request.baseline, "baseline"); + const base = requireJsonObject(baseline.target, "baseline target"), context = requireJsonObject(baseline.context, "baseline context"); + if (delivery.schema_version !== VERSION || delivery.mode !== "delta" || !equal(delivery.target, target) + || !equal(delivery.base, base) || !compatible(base, target) || base.context_digest !== digest(context) + || !Number.isSafeInteger(baseline.ancestry) || Number(baseline.ancestry) < 0 + || delivery.ancestry !== Number(baseline.ancestry) + 1 || Number(delivery.ancestry) > MAX_ANCESTRY) return fallback(); + const before = freshPaths(context, surface, legacyQuotaFields), after = freshPaths(currentContext, surface, legacyQuotaFields); + if (!before.valid || !after.valid) return fallback(); + const expected = changeTree(context, currentContext, after.paths); + // Also rejects omitted unchanged controls, redundant/conflicting edits, + // malformed array indices/lengths and unsupported operations before applying. + if (!equal(delivery.changes, expected)) return fallback(); + const result = applyTree(context, expected); + if (!equal(result, currentContext)) return fallback(); + return {...clone(delivery), context: result}; + } catch { return fallback(); } +} diff --git a/loopx/control_plane/quota/turn_envelope.py b/loopx/control_plane/quota/turn_envelope.py index 02aa223b5a..fa53d3ce98 100644 --- a/loopx/control_plane/quota/turn_envelope.py +++ b/loopx/control_plane/quota/turn_envelope.py @@ -105,3 +105,32 @@ def bind_turn_decision_capture( captured_decision_path=captured_decision_path, turn_instance_id=turn_instance_id, turn_key=turn_key, ) + + +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, + surface: str = "work_context", +) -> dict[str, Any]: + """Prepare at a Host's final input boundary, never from CLI self-attestation. + + 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. + """ + 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) + + +def restore_context_delivery( + delivery: Mapping[str, Any], *, baseline: Mapping[str, Any] | None, + current: Mapping[str, Any], +) -> dict[str, Any]: + """Validate/reconstruct or return the same-capture full body without effects.""" + return _turn_envelope_result("context_recovery", delivery=dict(delivery), + baseline=dict(baseline) if baseline is not None else None, + current=dict(current)) diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 4978401a9b..105e21117f 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -1,6 +1,7 @@ import { EffectiveAction } from "./effective_action.generated.ts"; import { createHash } from "node:crypto"; import { ENVELOPED_SHA256_PATTERN } from "../content_digest.ts"; +import {prepareContextDelivery, restoreContextDelivery} from "./context_delivery.ts"; import { interpretQuotaShouldRunPacket, @@ -44,6 +45,9 @@ export function readMonitorHeartbeatRecommendation(decision: JsonObject): JsonOb export function monitorHeartbeatDecisionFields(decision: JsonObject): JsonObject { return {heartbeat_recommendation: readMonitorHeartbeatRecommendation(decision)}; } +// The legacy adapter owns its field names. The codec receives this local binding +// from the adapter, never from request data or a second legacy-field reader. +const LEGACY_QUOTA_CONTEXT_FIELDS = Object.keys(monitorHeartbeatDecisionFields({})); /** Keep legacy heartbeat fields at their existing quota projection boundary; * the interaction owner still decides required reads and scheduler arbitration. */ @@ -998,6 +1002,8 @@ export function evaluateTurnEnvelope(value: unknown): JsonObject { const request = requireJsonObject(value, "turn envelope request"); const operation = request.operation; if (operation === "build") return buildTurnEnvelope(request); + if (operation === "context_delivery") return prepareContextDelivery(request, LEGACY_QUOTA_CONTEXT_FIELDS); + 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( requireJsonObject(request.envelope, "turn_envelope"), request.turn_instance_id, diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py new file mode 100644 index 0000000000..229d5620b1 --- /dev/null +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -0,0 +1,352 @@ +"""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. +""" +from __future__ import annotations + +from contextlib import contextmanager +from collections.abc import Iterator +from typing import Any +import json +import os +from pathlib import Path +import stat +import uuid + +from .decision_capture import write_decision_capture, project_captured_context, CONTEXT_CAPTURE_READ_GUIDANCE + +# 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") + + +def context_delivery_activation(setting: str, *, runtime_profile: str | None = None, + scheduler_execution_context: dict[str, Any] | None = None, + benchmark_mode: str | None = None) -> dict[str, Any]: + from ..scheduler.execution_context import ( + ExecutionMode, HostSurface, SchedulerOwner, resolve_scheduler_execution_context, + scheduler_execution_context_for_runtime_profile, + ) + + if setting not in CONTEXT_DELIVERY_SETTINGS: + raise ValueError("context-delivery must be off or on") + scenario = None + if setting == "on": + # Conflicting declarations cannot activate transport. The scheduler + # owner still validates execution; this adapter does not repair it. + if runtime_profile is not None and scheduler_execution_context is not None: + resolution = resolve_scheduler_execution_context(None) + elif runtime_profile is not None: + resolution = scheduler_execution_context_for_runtime_profile(runtime_profile) + else: + resolution = resolve_scheduler_execution_context(scheduler_execution_context) + context = resolution.context if resolution.ok else None + if context is not None: + if (context.host_surface is HostSurface.CODEX_APP + 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.GENERIC_CLI + and context.scheduler_owner is SchedulerOwner.AGENT_CLI_LOOP + and context.execution_mode is ExecutionMode.INTERACTIVE + and benchmark_mode == "heartbeat"): + scenario = "codex_benchmark_heartbeat" + if scenario is None: + raise ValueError( + "--context-delivery on requires a supported declared run mode: " + "Codex App heartbeat (--codex-app or --runtime-profile codex_app_heartbeat), " + "or the Codex benchmark heartbeat worker. Missing, contradictory or unsupported " + "declarations are rejected; use --context-delivery off for ordinary full delivery." + ) + return {"requested": setting, "enabled": scenario is not None, "scenario": scenario, + "reason": "supported_declared_run_mode" if scenario else "disabled", + "supported_scenarios": list(CONTEXT_DELIVERY_SCENARIOS)} + + +def _read(path: Path, *, private: bool = True) -> dict[str, Any]: + from ..effect_runtime import MAX_REQUEST_BYTES + + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + 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 (private and info.st_mode & 0o077) or info.st_size > MAX_REQUEST_BYTES): + raise ValueError("context transport needs a bounded private regular file") + raw = stream.read(MAX_REQUEST_BYTES + 1) + if len(raw) > MAX_REQUEST_BYTES: + raise ValueError("context transport file is too large") + result = json.loads(raw) + if not isinstance(result, dict): + raise ValueError("context transport file must contain an object") + return result + + +def _private_root(directory: str) -> Path: + root = Path(directory).expanduser().absolute() + root = root.parent.resolve(strict=True) / root.name + 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 directory must already exist, be private and belong to this user") + return root + + +def codex_context_directory(*, directory: str | None = None, root: str | None = None, + session: str | None = None, create: bool = False) -> str | None: + """Namespace shared host configuration without sharing session journals. + + A native session id selects a directory, never supplies a filesystem path + or proves retained context. Read-only recovery must not create directories. + """ + if directory is not None and root is not None: + raise ValueError("choose codex-context-dir or codex-context-root") + if root is None: + return directory + import hashlib + + parent = _private_root(root) + session = session if session is not None else os.environ.get("CODEX_THREAD_ID") + if not isinstance(session, str) or not session: + raise ValueError("native session context requires CODEX_THREAD_ID or a native hook session") + child = parent / hashlib.sha256(session.encode()).hexdigest() + if create: + child.mkdir(mode=0o700, exist_ok=True) + return str(_private_root(str(child))) + + +@contextmanager +def _locked(directory: str) -> Iterator[Path]: + if os.name != "posix": + raise ValueError("native context continuity is unavailable on this platform") + import fcntl + + root = _private_root(directory) + fd = os.open(root / "lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600) + with os.fdopen(fd, "r+") as lock: + if not stat.S_ISREG(os.fstat(lock.fileno()).st_mode): + raise ValueError("context lock is not a regular file") + fcntl.flock(lock, fcntl.LOCK_EX) + try: + yield root + finally: + fcntl.flock(lock, fcntl.LOCK_UN) + + +def _state(root: Path) -> dict[str, Any]: + try: + return _read(root / "session.json") + except FileNotFoundError: + return {} + + +def _save(root: Path, state: dict[str, Any]) -> None: + write_decision_capture(root / "session.json", state, publish=os.replace) + + +def read_codex_context_reference(directory: str, *, goal_id: str | None, agent_id: str | None) -> tuple[str, str]: + """Select an original capture after a lost hook; never assert retention. + + The journal is atomically replaced, so this read needs no lock or write. + Later pages use the returned explicit path/digest, not a moving selector. + """ + state = _state(_private_root(directory)) + session = os.environ.get("CODEX_THREAD_ID") + if (not session or state.get("session_id") != session + or not isinstance(state.get("cwd"), str) + or Path(state["cwd"]).resolve() != Path.cwd().resolve()): + raise ValueError("original capture lookup requires the same native session and workspace") + recovery = state.get("recovery") + if (not goal_id or not agent_id or not isinstance(recovery, dict) + or recovery.get("goal_id") != goal_id or recovery.get("agent_id") != agent_id): + raise ValueError("no original capture for this Goal/Agent; use its explicit capture reference") + path, digest = recovery.get("path"), recovery.get("sha256") + if not isinstance(path, str) or not isinstance(digest, str): + raise ValueError("original capture reference is incomplete") + return path, digest + + +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.""" + import shlex + + command = shlex.join([*cli_argv, "--format", "json", "quota", "context-read", + "--codex-context-root" if session_root else "--codex-context-dir", directory, + "--goal-id", goal_id, "--agent-id", agent_id]) + 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 + ". " + "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." + ) + + +def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, + cli_argv: list[str], scenario: str, session_root: bool = False) -> dict[str, Any]: + """Native configuration data shared by the worker and CLI setup journey. + + This renders no trust hashes and writes no host configuration. The native + host remains responsible for reviewing/trusting the exact discovered hook. + """ + import shlex + + if scenario not in CONTEXT_DELIVERY_SCENARIOS: + raise ValueError("unsupported native context configuration scenario") + if not goal_id or not agent_id: + raise ValueError("context configuration requires exact goal-id and agent-id") + directory = str(_private_root(directory)) + option = "--codex-context-root" if session_root else "--codex-context-dir" + handler = {"type": "command", "command": shlex.join([ + *cli_argv, "--format", "json", "quota", "context-hook", "--context-delivery", "on", + *(["--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")}, + "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. + + 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. + """ + 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 {} + 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") + 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"]}} + 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 + " " + + recovery["command"]}} + return {} + + +def _trace_bundle(session: str, tool_id: str) -> str | None: + from .codex_input_trace import read_native_trace_file + + 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 + 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 + + +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]: + """Existing CLI consumer, sharing the rendezvous projector and recovery IO. + + Serialization prevents an invalidation from moving the epoch during + preparation. The current capture reference is durable before publication, + so a later compact event recovers those exact bytes. Full candidates are + never advanced from file existence, hook success or a saved delivery flag. + """ + from .codex_input_trace import codex_context_in_active_input + + if directory is None: + return payload + try: + with _locked(directory) as root: + state = _state(root) + session, epoch = state.get("session_id"), state.get("context_epoch") + if (not isinstance(session, str) or not isinstance(epoch, str) + or not session or not epoch or session != os.environ.get("CODEX_THREAD_ID") + or Path(state.get("cwd", "")).resolve() != Path.cwd().resolve()): + return payload + scope = dict(goal_id=goal_id, agent_id=agent_id, session_id=session, context_epoch=epoch) + request: dict[str, Any] = {"scope": scope} + candidate = state.get("candidate") + if isinstance(candidate, dict) and isinstance(state.get("active_call"), str): + try: + baseline = _read(Path(candidate["path"])) + trace = _trace_bundle(session, 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=session): + request.update(baseline=baseline, retained={"complete": True, "target": baseline["target"]}, + native_input=observation) + except (OSError, ValueError, KeyError, TypeError, RecursionError): + pass # Missing candidate: publish a new full capture, never an old recovery ref. + result = project_captured_context(payload, directory=capture_directory, request=request, + goal_id=goal_id, agent_id=agent_id, surface=surface, capture_id=capture_id) + if result.get("schema_version") != "context_delivery_v1": + return result + recovery = result["recovery"] + state["recovery"] = {**{key: recovery[key] for key in ("path", "sha256", "command")}, + "goal_id": goal_id, "agent_id": agent_id} + if 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"]) + _save(root, state) + return result + except (OSError, ValueError, KeyError, TypeError, RecursionError): + # A broken optional adapter preserves the already admitted full result. + return payload diff --git a/loopx/control_plane/turn_driver/codex_input_trace.py b/loopx/control_plane/turn_driver/codex_input_trace.py index 2f28de6682..9452ab5f0b 100644 --- a/loopx/control_plane/turn_driver/codex_input_trace.py +++ b/loopx/control_plane/turn_driver/codex_input_trace.py @@ -4,9 +4,220 @@ import hashlib import json import os +import stat from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path +from typing import Any + + +def read_native_trace_file(path: Path, *, tail: bool = False) -> bytes: + from ..effect_runtime import MAX_REQUEST_BYTES + + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + 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 and not tail): + raise ValueError("native observation is not a bounded regular file") + offset = max(0, metadata.st_size - MAX_REQUEST_BYTES) if tail else 0 + stream.seek(offset) + raw = stream.read(MAX_REQUEST_BYTES + 1) + if len(raw) > MAX_REQUEST_BYTES: + raise ValueError("native observation exceeds the transport limit") + # Only the latest in-flight inference is relevant. Bound IO even for a + # long-running trace, discarding its cut first row, never its last row. + return raw.split(b"\n", 1)[-1] if offset else raw + + +def _canonical_native_json(value: object) -> object: + # JSON has one number type; native Python projection may spell 1.0 + # where the TS codec spells 1. Preserve booleans and numeric values. + if isinstance(value, float) and value.is_integer(): + return int(value) + if isinstance(value, list): + return [_canonical_native_json(item) for item in value] + if isinstance(value, dict): + return {key: _canonical_native_json(item) for key, item in value.items()} + return value + + +def _encoded_native_document(value: object) -> str: + return json.dumps(_canonical_native_json(value), sort_keys=True, ensure_ascii=False, allow_nan=False) + + +def _native_tool_is_bound(tail: list[dict[str, Any]], completed: list[dict[str, Any]], tools: list[dict[str, Any]], output: dict[str, Any], tool_id: str) -> bool: + tool = tools[0]["payload"] + if tool.get("requester") == {"type": "model"}: + bound = (tool.get("model_visible_call_id") == tool_id + and tool.get("code_mode_runtime_tool_id") is None + and any(item.get("type") == "function_call" and item.get("call_id") == tool_id + and item.get("name") == "exec_command" for item in output.get("output_items", []))) + else: + # A code-mode child has no model-visible call id of its own. Bind + # the live child to its native cell and that cell's exact model + # output; a child id or a saved source string alone proves nothing. + requester = tool.get("requester", {}) + cell_id = requester.get("runtime_cell_id") + cells = [row for row in tail if row["payload"].get("type") == "code_cell_started" + and row["payload"].get("runtime_cell_id") == cell_id] + bound = (requester.get("type") == "code_cell" and isinstance(cell_id, str) and bool(cell_id) + 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 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" + and row["payload"].get("runtime_cell_id") == cell_id for row in tail) + and any(item.get("type") == "custom_tool_call" and item.get("name") == "exec" + and item.get("call_id") == cells[0]["payload"].get("model_visible_call_id") + and item.get("input") == cells[0]["payload"].get("source_js") + for item in output.get("output_items", []))) + return bound + + +def _model_input_texts(request: dict[str, Any]) -> Iterator[str]: + for item in request["input"]: + if item.get("type") == "function_call_output" and isinstance(item.get("output"), str): + yield item["output"] + elif item.get("type") == "custom_tool_call_output" and isinstance(item.get("output"), list): + for part in item["output"]: + if part.get("type") == "input_text" and isinstance(part.get("text"), str): + yield part["text"] + + +def _read_bundle_payload(bundle: Path, ref: dict[str, Any]) -> dict[str, Any]: + relative = Path(ref["path"]) + if (relative.is_absolute() or len(relative.parts) != 2 or relative.parts[0] != "payloads" + or ".." in relative.parts): + raise ValueError("native payload reference escapes its bundle") + path = bundle / relative + path.resolve(strict=True).relative_to(bundle) + value = json.loads(read_native_trace_file(path)) + if not isinstance(value, dict): + raise ValueError("native payload must be an object") + 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. + + Native trace v1 is an optional, provider-specific IO boundary. Only an + already enabled trace is read; this neither enables tracing nor modifies a + session. A saved history, an earlier inference or a completed tool is not + current-input evidence. Missing/partial/unknown data returns false. + + This observes the input that produced this tool call, not a future model + dispatch. The caller still owns lifecycle invalidation and same-capture + recovery. It must not use this function as a standalone continuity lease. + """ + try: + if baseline.get("mode") != "full" or baseline.get("target", {}).get("session_id") != session_id: + return False + # Python equality conflates booleans and numbers; source observation + # must retain their JSON types, as the shared projection owner does. + expected = _encoded_native_document(baseline) + context = _encoded_native_document(baseline.get("context")) + bundle = Path(observation["trace_bundle"]).resolve(strict=True) + manifest = json.loads(read_native_trace_file(bundle / "manifest.json")) + if (manifest.get("schema_version") != 1 or manifest.get("root_thread_id") != session_id + or manifest.get("rollout_id") != session_id): + return False + + raw_events = read_native_trace_file(bundle / "trace.jsonl", tail=True) + if not raw_events.endswith(b"\n"): + return False # a partial writer tail cannot establish the current cursor + rows = [json.loads(line) for line in raw_events.splitlines()] + if any(row.get("schema_version") != 1 or type(row.get("seq")) is not int + or row.get("rollout_id") != session_id for row in rows): + return False + if any(a["seq"] >= b["seq"] for a, b in zip(rows, rows[1:])): + return False + rows = [row for row in rows if row.get("thread_id") == session_id] + starts = [i for i, row in enumerate(rows) if row["payload"].get("type") == "inference_started"] + if not starts: + return False + tail = rows[starts[-1]:] + turn = tail[0]["codex_turn_id"] + inference = tail[0]["payload"]["inference_call_id"] + tool_id = observation["tool_call_id"] + completed = [row for row in tail if row["payload"].get("type") == "inference_completed" + 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"]: + 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) + for row in tail): + return False + output = _read_bundle_payload(bundle, completed[0]["payload"]["response_payload"]) + bound = _native_tool_is_bound(tail, completed, tools, output, tool_id) + if not bound: + return False + request = _read_bundle_payload(bundle, tail[0]["payload"]["request_payload"]) + if request.get("previous_response_id"): + return False # implicit server-side history is outside this observer + raw = read_native_trace_file(Path(observation["capture_file"])) + digest = hashlib.sha256(raw).hexdigest() + captured = _encoded_native_document(json.loads(raw)) + if digest != observation["capture_sha256"] or captured not in (expected, context): + return False + ranges: list[tuple[int, int]] = [] + + for text in _model_input_texts(request): + # The qualified exec transport wraps complete output after this + # delimiter. Ignore prose/quoted JSON, previews and other item roles. + text = text.split("\nOutput:\n", 1)[-1] + try: + page = json.loads(text) + # text(await tools.exec_command(...)) preserves the native + # result envelope as one text block. Unwrap once, never scan + # arbitrary quoted prose or use the trace's private result. + if (isinstance(page, dict) and type(page.get("exit_code")) is int + and page["exit_code"] == 0 and isinstance(page.get("output"), str)): + page = json.loads(page["output"]) + except ValueError: + continue + document = _encoded_native_document(page) + if document == expected or (captured == context and document == context): + return True + if isinstance(page, dict): + page_range = _capture_page_range(page, raw=raw, digest=digest) + if page_range is not None: + ranges.append(page_range) + cursor = 0 + for start, end in sorted(ranges): + if start > cursor: + return False + # Reducing page size after truncation changes boundaries. Exact + # overlapping ranges are safe: each was compared to the immutable + # capture, not another page or a saved delivery acknowledgement. + cursor = max(cursor, end) + return cursor == len(raw) + except (OSError, ValueError, KeyError, TypeError, AttributeError, RecursionError): + return False def record_codex_input_source( diff --git a/loopx/control_plane/turn_driver/decision_capture.py b/loopx/control_plane/turn_driver/decision_capture.py index d7d122868f..79d078107d 100644 --- a/loopx/control_plane/turn_driver/decision_capture.py +++ b/loopx/control_plane/turn_driver/decision_capture.py @@ -1,7 +1,9 @@ -"""Private observation IO for the real Turn host; TS owns binding validation.""" +"""Private original-capture and presentation IO; TS owns binding validation.""" from __future__ import annotations +import hashlib import json +import shlex import os import socket import stat @@ -91,3 +93,77 @@ def validate_handoff_socket(value: str) -> Path: except OSError as exc: raise QuotaCommandValidationError(f"invalid decision handoff socket: {exc}") from exc return 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 " + "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." +) + + +def context_capture_read_command(path: Path, digest: str, *, offset: int = 0, page_bytes: int = 4096) -> 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)]) + + +def project_captured_context( + payload: dict[str, Any], *, directory: Path | None, request: dict[str, Any] | None, + goal_id: str, agent_id: str, surface: str, capture_id: str, +) -> dict[str, Any]: + """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 + opt-in attempt saves the complete projected JSON for same-capture recovery; + neither malformed requests nor optimization failures rerun the guard. + """ + if request is None or directory is None: + return payload + from ..effect_runtime import EffectRuntimeRemoteError, EffectRuntimeStartupError + from ..quota.turn_envelope import prepare_context_delivery + + try: + projection = directory / "projection.json" + write_decision_capture(projection, payload) + scope = request.get("scope") + 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") + if "native_input" in request: + from .codex_input_trace import codex_context_in_active_input + + # An optional native observation can only narrow the caller's + # retention claim. It never mints an ACK or enables tracing, and a + # malformed/stale observation must not fall back to that claim. + observation, baseline = request["native_input"], request.get("baseline") + native_session = scope.get("session_id") + if (not isinstance(observation, dict) or not isinstance(baseline, dict) + or not isinstance(native_session, str) + or not codex_context_in_active_input(observation, baseline=baseline, + session_id=native_session)): + retained = None + delivery = prepare_context_delivery(payload, scope=scope, + capture_id=capture_id, surface=surface, + baseline=request.get("baseline"), retained=retained) + delivery["recovery"] = { + "path": str(projection), "sha256": hashlib.sha256(projection.read_bytes()).hexdigest(), + "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, + } + delivery["recovery"]["command"] = context_capture_read_command(projection, delivery["recovery"]["sha256"]) + return delivery + except (OSError, ValueError, TypeError, EffectRuntimeRemoteError, EffectRuntimeStartupError): + # The original full payload is already available. An unavailable optional + # codec/capture must not convert safe full delivery into an execution gate. + return payload diff --git a/loopx/control_plane/work_items/interaction_contract.ts b/loopx/control_plane/work_items/interaction_contract.ts index b4905fd499..487a218846 100644 --- a/loopx/control_plane/work_items/interaction_contract.ts +++ b/loopx/control_plane/work_items/interaction_contract.ts @@ -19,6 +19,7 @@ import { AgentScopeFrontierAction } from "../agents/agent_scope_frontier.generat import { resumeConditionHasKnownPendingTarget } from "../todos/resume_condition.ts"; import { parseTodoTimestampMicros } from "../runtime_timestamp.ts"; import { monitorIsActive } from "../todos/summary_lanes.ts"; +import {compactSelectedTodoSource} from "../quota/context_delivery.ts"; function exactBlockedSuccessorWait(request: JsonObject, monitorWait = false): JsonObject | null { const wait = jsonObject(request.vision_wait_state); @@ -763,7 +764,7 @@ export function projectInteractionWorkContext(request: JsonObject): JsonObject { const matches = results.filter(result => result.command === read.command); if (matches.length === 0) { pending.push(read); continue; } const result = matches[0]; - const content = jsonObject(result.content); + let content = jsonObject(result.content); let valid = matches.length === 1 && content !== null && content.ok !== false && !result.error_code; let selectedTodoRecord: JsonObject | null = null; if (valid && read.source === "selected_todo") { @@ -789,6 +790,10 @@ export function projectInteractionWorkContext(request: JsonObject): JsonObject { if (valid && read.source === "goal_acceptance") { valid = (!!configuredGoalChecks(jsonObject(content!.goal_acceptance_contract)) || !!jsonObject(content!.goal_acceptance_contract)?.outcome_requirements || !!jsonObject(content!.goal_acceptance_contract)?.goal_objective); } + if (valid && read.source === "selected_todo") { + content = compactSelectedTodoSource(content!); + selectedTodoRecord = jsonObject(content.todo); + } if (!valid) { pending.push(read); failures.push({source: read.source ?? read.kind, command: read.command, diff --git a/loopx/hosts/codex_checkpoint.py b/loopx/hosts/codex_checkpoint.py index 76475ea0a2..37fba19ddc 100644 --- a/loopx/hosts/codex_checkpoint.py +++ b/loopx/hosts/codex_checkpoint.py @@ -116,7 +116,10 @@ def arguments(command): "goal_id": guard["goal_id"], "agent_id": guard["agent_id"], "turn_instance_id": guard["turn_instance_id"], "turn_envelope": turn_envelope, "decision_output_root": guard["capture_root"], - "decision_output_dir": None, "decision_handoff_socket": guard["socket_path"]} + "decision_output_dir": None, "decision_handoff_socket": guard["socket_path"], + "codex_context_dir": guard.get("codex_context_dir"), + "codex_context_root": guard.get("codex_context_root"), + "context_delivery": guard.get("context_delivery", "off")} if any(args.get(key) != value for key, value in expected.items()): raise ValueError("native pending guard differs from the held packet routes or identity") # A second matching guard after a tool yield cannot stand in for the first diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 186ed398f4..5f0f4132d7 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -20,7 +20,8 @@ teaches a reusable control-plane lesson. | `cli_json_file_argument_contract_gap` | A `--*-json` option repeatedly rejects inline JSON as an unreadable object; reshaping it does not help, or Markdown shows only an invalid-request status. | The command's current help, file-input loader, structured error, Markdown renderer, and a synthetic valid-file recovery. | The option accepts a file path but help does not say so; file-read, JSON-decode and root-shape failures are conflated, or the renderer omits the error reason. | Clarify the existing file contract without silently adding inline input. Separate read, parse and shape diagnostics without echoing supplied contents or paths; preserve the reason in each output format. Validate missing/inline input, malformed/non-object files, same-path recovery and unchanged typed admission. Repair the owning CLI adapter, not provider readiness or Goal retry authority. | | `public_source_receipt_clock_precedes_reads` | Multiple sources share a batch-start observation time, or a receipt reports completion before its final source read returns. | Installed provider source pin, actual execution process bounds, source/receipt timestamps, a controlled advancing-clock case and canonical readback. | The provider captures one UTC timestamp before its HTTP loop and reuses it for source observations and completion. | Record each successful source observation after its bounded read and record completion after all attempts, including partial, empty and failed results. Keep publication/first-availability unknown, retain original receipts and reuse the existing typed identity/admission/projection owner. Verify late failures cannot move the completion boundary before an earlier successful observation; do not claim authenticated execution or downstream coverage from timestamp correctness. | | `public_source_failure_step_erasure` | A public-source plan is ready, but execution reports only a generic unavailable read, hiding whether visibility, download or text validation failed. | Original exact-plan receipt, source index, read-only process bounds, provider source pin and shared readback. | One catch path erased the reached provider step; readiness was mistaken for successful retrieval or the generic error was guessed to mean lost permission. | Preserve the actual reached step in existing redacted limitations, without exposing exception reasons or changing admission, retry or source status. Cover visibility, download, malformed text and partial HTTP failure through the canonical CLI and shared sinks. Keep original failed receipts; diagnose a changed producer under a bounded trial rather than repeatedly fetching the same object or freezing independent work. | -| `candidate_outcome_as_execution_blocker` | An evaluation repeatedly enters a bounded retry after rejecting individual candidates, although independent authorized evaluation remains executable. | Exact selected Todo, typed delivery outcome and result class, committed retry origin/due time, validated evidence delta, current vision/frontier and installed source pin. | The caller classified candidate uncertainty or rejection as blocked execution for the whole work item; a stale runtime may also explain the retry as an unrelated user gate. | Correct the existing Todo/vision and classify new validated evaluation evidence as progress only when it actually advances acceptance. Stop answered finite questions; keep pure reuse non-progress. Preserve genuine blocked retries and independent clocks. Reproduce retry versus ordinary notice through the existing quota/interaction/scheduler path, verify the installed revision contains the owner fix, and retain old receipts. Never remove cooldowns globally or manufacture progress. | +| `context_delta_narrow_eligibility` | Warm context reuse saves little despite large unchanged adjacent packets. | Same captured workload, complete wire bytes, exact subtree changes, fresh controls, cold starts, metadata and full recovery costs. | A body-only split excludes most repeated projections, atomic arrays resend unchanged siblings, and a separate fresh frame duplicates fields already present in patches. | Measure eligibility and overhead separately before changing source semantics. Reuse the existing typed full/delta owner across complete quota and Turn projections; compare arrays against the exact hashed baseline without fuzzy identity or timestamp normalization. Keep current authority and unknown controls inline, validate exact reconstruction, and automatically use the same capture on loss or mismatch. Include cold full inputs and recovery reads; offline byte potential is not installed solver input or outcome evidence. | +| `candidate_outcome_as_execution_blocker` | An evaluation repeatedly enters a bounded retry after rejecting individual candidates, although independent authorized evaluation remains executable. | Exact selected Todo, typed delivery outcome and result class, committed retry origin/due time, validated evidence delta, current vision/frontier and installed source pin. | The caller classified candidate uncertainty or rejection as blocked execution for the whole work item; a stale runtime may also explain the retry as an unrelated user gate. | Correct the existing Todo/vision and classify new validated evaluation evidence as progress only when it actually advances acceptance. Stop answered finite questions; keep pure reuse non-progress. Scope a missing dependency to the acceptance it actually prevents: missing experiment cost data or one Host integration must not block independent shared implementation, local validation or another authorized consumer. A fault-injection failure calls for a bounded recovery implementation and retest, not an assumed requirement to wait for a new Host interface. Preserve genuine blocked retries and independent clocks. Reproduce retry versus ordinary notice through the existing quota/interaction/scheduler path, verify the installed revision contains the owner fix, and retain old receipts. Never remove cooldowns globally or manufacture progress. | | `due_monitor_display_order_starvation` | A recurring Monitor is selected on every wake while equally ranked older due Monitors never run. | Complete eligible due set, claim/profile/priority ranks, due instants, display caps, selected Todo and exact poll/Turn receipts. | Display index determines execution order before a one-row cap; the first Monitor becomes due again before the next admitted Turn. | In the existing typed planning owner, retain scope and eligibility fences, then rank equally eligible work by due instant before display index and truncation. Cover recurring reschedules, stable ties, capability/claim negatives and real provider replay. Preserve exact per-Turn settlement; fairness does not authorize same-wake loops, extra quota spend or a shorter owner cadence. | | `receipt_freshness_commit_gap` | A receipt-bound follow-up passes its first freshness check, then commits an older Turn after a newer receipt arrives during lock contention or final persistence. An immediate retry correctly reports the old Turn as stale. | Ordered receipt events, the exact Turn ID, state-lock timing, commit result, and persisted state identity. | Receipt freshness and scheduler persistence did not hold the same cross-runtime append fence, so a newer receipt could arrive after the final check. | Keep the early check for fast rejection. Acquire the scheduler-state lock before the receipt-log mutation lock, then retain both locks from the final receipt read through legacy migration and the final state rename. Make every receipt writer use the same mutation lock. Return the existing typed missing or stale failure without mutation. Prove both lock-wait and read-to-rename interleavings with real writers, then scan for reverse lock order. | | `notification_receipt_scope_and_frontier_gap` | A recovered blocker never notifies again, later notices starve, or a new channel claims delivery from an old receipt. | Canonical blocked/open/blocked transitions, full candidate frontier, destination-bound private receipts, and public readback. | Receipt lookup omitted destination/generation, recovery used an incomplete status list, or the batch limit was applied before delivered effects were removed. | Reuse canonical status and notice builders; retire only explicit recovery or supersession facts, preserve missing-row uncertainty, bind receipts to destination and durable generation, and budget pending effects with fair retries. Validate repeated same-clock recovery, more than one batch, persistent failures, target switches and default-off parity; keep historical receipts private. | diff --git a/tests/control_plane/test_codex_context_boundary.py b/tests/control_plane/test_codex_context_boundary.py index 496416e365..8225c735cd 100644 --- a/tests/control_plane/test_codex_context_boundary.py +++ b/tests/control_plane/test_codex_context_boundary.py @@ -66,11 +66,18 @@ def _usage(request, number): def _input_text(request): """Read actual model-input text; tools/instructions are not source delivery.""" - return "\n".join(part.get("text", "") for item in request["input"] - for part in item.get("content", []) if isinstance(part, dict)) + text = [] + for item in request["input"]: + output = item.get("output") if item.get("type") == "function_call_output" else None + if isinstance(output, str): + text.append(output) + for part in output if isinstance(output, list) else item.get("content", []): + if isinstance(part, dict): + text.append(part.get("text", "")) + return "\n".join(text) -def _app_server(binary, env, workspace, home, trusted, additional_context=None): +def _app_server(binary, env, workspace, home, trusted, additional_context=None, *, sandbox="read-only", resume=None): 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, @@ -78,7 +85,7 @@ def _app_server(binary, env, workspace, home, trusted, additional_context=None): native.request("initialize", {"clientInfo": {"name": "context_boundary_fixture", "version": "0.1.0"}, "capabilities": {"experimentalApi": True}}) native.notify("initialized", {}) - params = {"cwd": str(workspace), "sandbox": "read-only", "approvalPolicy": "never"} + params = {"cwd": str(workspace), "sandbox": sandbox, "approvalPolicy": "never"} if trusted: # Trust only the exact synthetic handlers created by this test. # Never read/alter the operator's hook trust or treat a global CLI @@ -89,7 +96,9 @@ def _app_server(binary, env, workspace, home, trusted, additional_context=None): 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}} - source = native.request("thread/start", params)["thread"]["id"] + if resume: + params["threadId"] = resume + source = native.request("thread/resume" if resume else "thread/start", params)["thread"]["id"] turn_params = {"threadId": source, "input": [{"type": "text", "text": "Synthetic context-delivery fixture."}]} if additional_context is not None: @@ -101,7 +110,7 @@ def _app_server(binary, env, workspace, home, trusted, additional_context=None): if event and event.get("method") == "turn/completed": assert event["params"]["turn"]["id"] == turn assert event["params"]["turn"]["status"] == "completed" - return + return source raise AssertionError("Native context fixture did not finish") @@ -126,6 +135,137 @@ def _configure_provider(home, url): 'wire_api="responses"\nrequires_openai_auth=false\n') +@pytest.mark.parametrize("host", ["exec", "app_server"]) +@pytest.mark.parametrize("failed_primary", [False, True]) +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_shared_delta_with_small_compact_recovery_reference(tmp_path, host, failed_primary, surface): + """Real native input: shared delta, small event hook, same-capture file read. + + The deterministic provider requests the necessary reads. This establishes + transport ordering/recovery, not autonomous solver adoption or token savings. + """ + from loopx.control_plane.quota.turn_envelope import ( + prepare_context_delivery, restore_context_delivery, + ) + binary, env, workspace, home = _native_fixture(tmp_path) + scope = dict(goal_id="fixture", agent_id="agent", session_id="synthetic-session", context_epoch="before-compact") + source = {"complete": True, "sources": [{"source": "selected_todo", "content": { + "todo": {"todo_id": "todo_fixture", "text": SOURCE, "note": "Stop before publishing.", "status": "open"}}}]} + target_source = json.loads(json.dumps(source)) + target_source["sources"][0]["content"]["todo"]["note"] = "Stop before publishing or deploying." + def projection(context): + if surface == "quota_packet": + return {"goal_id": "fixture", "agent_id": "agent", "should_run": True, + "interaction_contract": {"agent_channel": {"delivery_allowed": True, "work_context": context}}} + return {"schema_version": "loopx_turn_envelope_v0", "action": {"delivery_allowed": True}, "work_context": context} + base_source, target_source = projection(source), projection(target_source) + base = prepare_context_delivery(base_source, scope=scope, capture_id="capture-a", surface=surface) + target = prepare_context_delivery(target_source, scope=scope, capture_id="capture-b", surface=surface) + delta = prepare_context_delivery(target_source, scope=scope, capture_id="capture-b", surface=surface, + baseline=base, retained={"complete": True, "target": base["target"]}) + assert delta["mode"] == "delta" + assert restore_context_delivery(delta, baseline=base, current=target)["context"] == target_source + files = {} + for name, value in (("base", base), ("target", target), ("delta", delta)): + files[name] = workspace / f"{name}.json" + files[name].write_text(json.dumps(value, separators=(",", ":"))) + saved = {name: path.read_bytes() for name, path in files.items()} + # The hook transports only a bounded invalidation/reference. A failed + # primary handler is caught by the small recovery wrapper; no big payload + # needs to survive the hook's default additionalContext limit. + hook = home / "hook.py" + hook.write_text( + "import json, pathlib, sys\n" + "event=json.load(sys.stdin)\n" + "if event.get('source') == 'compact':\n" + f" pathlib.Path({str(home / 'epoch-invalidated')!r}).write_text('compact')\n" + " try:\n" + + (" raise RuntimeError('injected primary handler failure')\n" if failed_primary else " pass\n") + + " except RuntimeError:\n pass\n" + f" reference={{'mode':'full_recovery','capture_id':'capture-b','path':{str(files['target'])!r}," + f"'context_digest':{target['target']['context_digest']!r}}}\n" + " print(json.dumps({'hookSpecificOutput':{'hookEventName':'SessionStart'," + "'additionalContext':'Baseline invalidated. Read this same-capture full source before dependent work; '" + "'do not rerun admission or ask the owner. '+json.dumps(reference)}}))\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")}})) + + # Native tool feedback can impose its own cap even when max_output_tokens + # is larger. Reconstruct from bounded pages, never a head/tail preview. + page_size = 6000 + def page_count(name): + return (len(saved[name].decode()) + page_size - 1) // page_size + base_pages, target_pages = page_count("base"), page_count("target") + compact_call = base_pages + 3 + final_call = compact_call + target_pages + 1 + + def page_command(name, index): + script = ("from pathlib import Path; import sys; " + f"sys.stdout.write('SOURCE_PAGE {name} {index}\\n' + " + f"Path({str(files[name])!r}).read_text()[{index * page_size}:{(index + 1) * page_size}])") + return shlex.join([sys.executable, "-c", script]) + + def response(request, number, message): + if number <= base_pages: + command = page_command("base", number - 1) + elif number == base_pages + 1: + command = page_command("delta", 0) + elif number == base_pages + 2: + command = "printf synthetic-work" + elif compact_call < number < final_call: + command = page_command("target", number - compact_call - 1) + else: + if number == compact_call: + message["content"][0]["text"] = "Synthetic summary with the original source deliberately removed." + return message + return {"type": "function_call", "id": f"fc_{number}", "call_id": f"call_{number}", + "name": "exec_command", "arguments": json.dumps({"cmd": command, "max_output_tokens": 2000})} + + def usage(request, number): + count = 30000 if number == base_pages + 2 else 1 + return {"input_tokens": count, "output_tokens": 1, "total_tokens": count + 1} + + with local_inference_provider(response, usage) as (url, calls, requests): + _configure_provider(home, url) + if host == "exec": + result = subprocess.run([binary, "exec", "--dangerously-bypass-hook-trust", "--skip-git-repo-check", "--json", + "-C", str(workspace), "Synthetic context-delivery fixture."], env=env, + capture_output=True, text=True, timeout=40) + assert result.returncode == 0, result.stderr + else: + _app_server(binary, env, workspace, home, True) + assert len(calls) == final_call + + def delivered_pages(request, name): + pages = {} + for item in request["input"]: + output = item.get("output") + if item.get("type") != "function_call_output" or not isinstance(output, str): + continue + marker = f"SOURCE_PAGE {name} " + if marker in output: + index, body = output.split(marker, 1)[1].split("\n", 1) + assert "truncated output" not in output + pages[int(index)] = body.rstrip("\n") + return "".join(pages[i] for i in range(len(pages))) + + assert delivered_pages(requests[base_pages], "base") == saved["base"].decode() + warm = requests[base_pages + 1] + assert delivered_pages(warm, "base") == saved["base"].decode() + assert delivered_pages(warm, "delta") == saved["delta"].decode() + assert delivered_pages(warm, "target") == "" + after_compact = requests[compact_call] + assert (home / "epoch-invalidated").read_text() == "compact" + assert 'Baseline invalidated.' in _input_text(after_compact) + assert str(files['target']) in _input_text(after_compact) + assert delivered_pages(after_compact, "base") == "" + assert delivered_pages(requests[-1], "target") == saved["target"].decode() + assert delivered_pages(requests[-1], "base") == "" + assert all(path.read_bytes() == saved[name] for name, path in files.items()) + + @pytest.mark.parametrize("host", ["exec", "app_server"]) @pytest.mark.parametrize("case", ["intact", "spill", "untrusted", "failed_recovery"]) def test_native_hook_context_boundary(tmp_path, host, case): diff --git a/tests/control_plane/test_codex_context_capture.py b/tests/control_plane/test_codex_context_capture.py new file mode 100644 index 0000000000..ee2f3c1a36 --- /dev/null +++ b/tests/control_plane/test_codex_context_capture.py @@ -0,0 +1,536 @@ +"""Native tools execute the actual quota CLI and recover its original capture. + +The loopback provider is a deterministic input-boundary observer. This qualifies +the opt-in production lifecycle/CLI composition, not installed adoption or solver benefit. +""" +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys + +import pytest + +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 +from test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _spend_run_count, _write_fixture +from tests.control_plane.codex_checkpoint_fixture import local_inference_provider +from tests.control_plane.test_heartbeat_envelope_transport import private_handoff # noqa: F401 -- shared private socket fixture +from tests.control_plane.test_codex_context_boundary import _app_server, _configure_provider, _input_text, _native_fixture + +pytestmark = pytest.mark.skipif(not os.environ.get("LOOPX_NATIVE_CHECKPOINT_BIN"), + reason="Explicit native Codex binary required; inference uses loopback only") + + +def _delivered(request, digest): + # Independent byte-map oracle: recover every model-visible byte, including + # separate code-mode emissions and overlapping reads at smaller page sizes. + delivered = {} + for item in request["input"]: + output = item.get("output") + texts = ([part["text"] for part in output if part.get("type") == "input_text"] + if item.get("type") == "custom_tool_call_output" and isinstance(output, list) + else [output]) + for output in texts: + if not isinstance(output, str): + continue + try: + page = json.loads(output.split("\nOutput:\n", 1)[-1]) + if isinstance(page, dict) and page.get("exit_code") == 0 and isinstance(page.get("output"), str): + page = json.loads(page["output"]) + except (ValueError, TypeError): + continue + if not isinstance(page, dict) or page.get("schema_version") != "context_capture_page_v1" or page.get("sha256") != digest: + continue + assert page["ok"] + data = page["content"].encode() + assert len(data) == (page["total_bytes"] if page["eof"] else page["next_offset"]) - page["offset"] + for offset, value in enumerate(data, page["offset"]): + assert offset not in delivered or delivered[offset] == value + delivered[offset] = value + if not delivered: + return "" + assert set(delivered) == set(range(len(delivered))) + raw = bytes(delivered[i] for i in range(len(delivered))) + assert hashlib.sha256(raw).hexdigest() == digest + return raw.decode().rstrip("\n") + + +@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")] + for failed in [False, True] +] + [("benchmark", surface, False, False, False) for surface in ("quota_packet", "turn_envelope")] + + [(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): + isolate_sqlite_runtime(tmp_path, monkeypatch) + binary, env, workspace, home = _native_fixture(tmp_path) + env["CODEX_ROLLOUT_TRACE_ROOT"] = str(workspace / "traces") + 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" + body = "\n".join(f"Requirement {i:04d}: preserve this row and its evidence." for i in range(400)) + body += "\nFINAL REQUIREMENT: stop before publication." + todos = read_fixture_todos(state) + todos[0].update(text=body, title=body) + 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="sqlite") + original_state = state.read_bytes() + root = workspace / "captures" + root.mkdir() + wire_paths = {name: workspace / f"{name}.json" for name in ("cold", "warm")} + lifecycle = workspace / "context" + lifecycle.mkdir(mode=0o700) + cli = ["env", f"PYTHONPATH={Path(__file__).resolve().parents[2]}", sys.executable, + "-m", "loopx.entrypoint", "--cli-bin", str(Path(sys.executable).with_name("loopx"))] + installed_cli = os.environ.get("LOOPX_NATIVE_CONTEXT_CLI") + if installed_cli: + cli = [installed_cli, "--cli-bin", installed_cli] + 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], + 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 failed_primary: + # Fault only the primary journal update. Production code must still + # return the original full-read reference; no test recovery handler. + hook = home / "hook.py" + hook.write_text( + "import json,sys\nfrom unittest.mock import patch\n" + "from loopx.control_plane.turn_driver.codex_context import codex_context_hook\n" + "event=json.load(sys.stdin)\n" + "if event.get('source')=='compact':\n" + " with patch('loopx.control_plane.turn_driver.codex_context._save', side_effect=OSError('synthetic failure')):\n" + f" result=codex_context_hook({str(lifecycle)!r},event)\n" + "else:\n" + f" result=codex_context_hook({str(lifecycle)!r},event)\n" + "print(json.dumps(result))\n") + hook_command = shlex.join(["env", f"PYTHONPATH={Path(__file__).resolve().parents[2]}", sys.executable, str(hook)]) + loss_marker = home / "lose-context-hook" + if hook_loss: + # Stop the whole hook process before product code runs. All later + # events fail too: recovery must not depend on a second working hook. + wrapper = home / "loopx-fixture" + wrapper.write_text("#!" + sys.executable + "\nimport os,signal,sys\n" + + f"if 'context-hook' in sys.argv and os.path.exists({str(loss_marker)!r}):\n" + + (" os.kill(os.getpid(), signal.SIGKILL)\n" if hook_loss == "killed" else " sys.exit(0)\n") + + (f"os.execv({installed_cli!r}, [{installed_cli!r}, *sys.argv[1:]])\n" if installed_cli else + 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)]) + handler = {"type": "command", "command": hook_command, "timeout": 10} + if host != "benchmark": + (home / "hooks.json").write_text(json.dumps({"hooks": { + event: [{"hooks": [handler]}] for event in ("SessionStart", "PreToolUse")}})) + 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)] + if handoff_socket: + command += ["--decision-handoff-socket", handoff_socket] + if surface == "turn_envelope": + command.append("--turn-envelope") + generated_guard = None + if shared_root: + generated = subprocess.run([*cli, "--registry", str(registry), "--runtime-root", str(runtime), + "--format", "json", "heartbeat-prompt", "--thin", "--codex-app", + "--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 [])], + capture_output=True, text=True, timeout=15) + assert generated.returncode == 0, generated.stderr + 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) + env["LOOPX_TURN"] = "native-capture-turn" + + checkpoint_wakes, held_captures = [], [] + + 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" + + def pages(name, path): + raw = path.read_bytes() + digest = hashlib.sha256(raw).hexdigest() + 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 + offsets, start, count = [0], 0, 0 + for character in raw.decode(): + size = len(character.encode()) + if count + size > page_size: + start += count + offsets.append(start) + count = 0 + count += size + if host == "benchmark" and name in {"cold", "recovery"}: + # Execute the shipped original recovery route, including its pinned + # executable; no fixture substitute for installed command routing. + recovery = wires["cold" if name == "cold" else "warm"]["recovery"] + routed = shlex.split(recovery["command"]) + assert routed[:3] == [env["LOOPX_CLI"], "--cli-bin", env["LOOPX_CLI"]] + assert routed[routed.index("--capture-file") + 1] == str(path) + routed[routed.index("--page-bytes") + 1] = "5000" + commands = [] + for offset in offsets: + routed[routed.index("--offset") + 1] = str(offset) + commands.append(shlex.join(routed)) + return commands + commands = [shlex.join(cli + ["--format", "json", "quota", "context-read", + "--capture-file", str(path), "--capture-sha256", digest, + "--offset", str(offset), "--page-bytes", str(page_size)]) for offset in offsets] + if adaptive and name == "cold": + # An intact initial short read remains in current native input when + # subsequent reads choose different boundaries. Production must + # accept exact complete coverage without clearing that history. + initial = shlex.split(commands[0]) + initial[-1] = "256" + commands.insert(0, shlex.join(initial)) + return commands + + def delivered(request, name): + return _delivered(request, page_digests[name]) + + def response(request, number, message): + nonlocal command, root + phase = stages["phase"] + if stages["pages"]: + cmd = stages["pages"].pop(0) + elif phase in {"start", "resuming"}: + if host == "benchmark": + # Read the actual worker-rendered guard, including its original + # wake/capture identity. No substitute dispatcher or guard. + line = [line for line in _input_text(request).splitlines() + if " quota should-run " in line and "--codex-context-dir" in line][-1] + native_guard = shlex.split(line) + assert native_guard[0] == env["LOOPX_CLI"] + assignment = [line for line in _input_text(request).splitlines() + if line.startswith("LOOPX_TURN=")][-1] + turn_id = shlex.split(assignment)[0].split("=", 1)[1] + command = [turn_id if arg == "${LOOPX_TURN:?}" else arg for arg in native_guard] + command += ["--todo-id", TODO_ID] + root = Path(command[command.index("--decision-output-root") + 1]) + 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") + 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") + 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] + stages["pages"] = pages(name, read_paths[name]) + stages["phase"] = name + "_read" + cmd = stages["pages"].pop(0) + elif phase == "cold_read": + assert delivered(request, "cold") == read_paths["cold"].read_text().rstrip("\n") + stages.update(baseline_confirmed=True, phase="warm_ready") + if resumed: + stages["phase"] = "resuming" + stages["cold_readback_sha256"] = hashlib.sha256(delivered(request, "cold").encode()).hexdigest() + message["content"][0]["text"] = stages["cold_readback_sha256"] + return 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") + stages.update(phase="compact", compact_response=number) + if hook_loss: + loss_marker.touch() + cmd = "printf synthetic-work" + elif phase == "compact": + stages["phase"] = "recovery_ready" + message["content"][0]["text"] = "Summary deliberately omitting all packet source bodies." + return message + elif phase == "recovery_ready": + if hook_loss: + text = _input_text(request) + assert "LoopX context baseline invalidated." not in text + # The recovery entry must really be present after compaction; + # 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] + stages["phase"] = "lookup_read" + assert delivered(request, "cold") == "" and delivered(request, "warm") == "" + return tool(cmd, number) + assert "LoopX context baseline invalidated." in _input_text(request) + assert wires["warm"]["recovery"]["command"] in _input_text(request) + assert delivered(request, "cold") == "" and delivered(request, "warm") == "" + stages["phase"] = "recovered" + stages["pages"] = pages("recovery", Path(wires["warm"]["recovery"]["path"])) + cmd = stages["pages"].pop(0) + 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 + assert page and page["ok"], page + assert page["sha256"] == wires["warm"]["recovery"]["sha256"] + page_digests["recovery"] = page["sha256"] + if not page["eof"]: + return tool(page["next_command"], number) + stages["phase"] = "recovered" + return response(request, number, message) + else: + assert phase == "recovered" + recovered = delivered(request, "recovery") + assert recovered == Path(wires["warm"]["recovery"]["path"]).read_text().rstrip("\n") + stages["phase"] = "done" + return message + return tool(cmd, number) + + def tool(cmd, number): + if host == "code_mode": + # Exercise the shipped default page size and bounded batching for + # both output forms. One model round per page can exhaust the + # unchanged native turn deadline before same-capture recovery ends. + commands = [cmd] + if stages["pages"]: + commands.append(stages["pages"].pop(0)) + if adaptive: + code = "".join("{const r=await tools.exec_command(" + json.dumps({ + "cmd": command, "max_output_tokens": 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": code} + return {"type": "custom_tool_call", "id": f"ctc_{number}", "call_id": f"call_{number}", + "name": "exec", "input": "".join("text(await tools.exec_command(" + json.dumps({ + "cmd": command, "max_output_tokens": 2000}) + "));" for command in commands)} + return {"type": "function_call", "id": f"fc_{number}", "call_id": f"call_{number}", + "name": "exec_command", "arguments": json.dumps({"cmd": cmd, "max_output_tokens": 2000})} + + def usage(request, number): + count = (2000000 if host == "benchmark" else 30000) if number == stages["compact_response"] else 1 + return {"input_tokens": count, "output_tokens": 1, "total_tokens": count + 1} + + def observed_response(request, number, message): + try: + return response(request, number, message) + except Exception as error: + # End the native fixture and report the original assertion, + # rather than making Codex retry a broken HTTP connection. + errors.append(error) + return message + + with local_inference_provider(observed_response, usage) as (url, calls, observed): + if host != "benchmark": + _configure_provider(home, url) + if host == "code_mode": + config = home / "config.toml" + config.write_text(config.read_text().replace("[features]\n", "[features]\ncode_mode=true\n")) + if native_configuration: + config = home / "config.toml" + config.write_text("developer_instructions=" + json.dumps(native_configuration["developer_instructions"]) + + "\n" + config.read_text()) + elif hook_loss: + from loopx.control_plane.turn_driver.codex_context import codex_context_recovery_instructions + config = home / "config.toml" + instructions = codex_context_recovery_instructions(str(lifecycle), goal_id=GOAL_ID, + agent_id=AGENT_ID, cli_argv=cli) + config.write_text("developer_instructions=" + json.dumps(instructions) + "\n" + config.read_text()) + # The real CLI talks to its isolated local TS Effect process. + # Native sandbox networking is disabled by default; allow this + # synthetic workspace's loopback transport explicitly. + if host != "benchmark": + with (home / "config.toml").open("a") as config: + config.write("[sandbox_workspace_write]\nnetwork_access=true\n") + if host == "exec": + result = subprocess.run([binary, "exec", "--dangerously-bypass-hook-trust", + "--skip-git-repo-check", "--json", "-s", "workspace-write", "-C", str(workspace), + "Synthetic CLI context-delivery fixture."], env=env, capture_output=True, text=True, timeout=90) + assert result.returncode == 0, result.stderr + elif host == "benchmark": + from benchmark.runtime.worker import run_once + skills = workspace / "skills" + skills.mkdir() + unrelated = project / "unrelated-hook-executed" + project_hooks = project / ".codex/hooks.json" + project_hooks.write_text(json.dumps({"hooks": {"SessionStart": [{"hooks": [{ + "type": "command", "command": shlex.join(["touch", str(unrelated)])}]}]}})) + env.update(LOOPX_CONTEXT_DELIVERY="on", LOOPX_TURN_ENVELOPE=str(int(surface == "turn_envelope")), + LOOPX_EXECUTION_MODE="heartbeat", LOOPX_ITERATION_CONTEXT="resume", + LOOPX_TASK_ENTRY="seeded-todo", LOOPX_TASK_STAGE="execute", + LOOPX_CODEX_HOME=str(home), LOOPX_PROJECT=str(project), LOOPX_SHARED_SKILLS=str(skills), + LOOPX_REGISTRY=str(registry), LOOPX_RUNTIME_ROOT=str(runtime), + LOOPX_GOAL_ID=GOAL_ID, LOOPX_AGENT_ID=AGENT_ID, LOOPX_WAKE_LOG_DIR=str(workspace / "wakes"), + LOOPX_CLI=str(wrapper if hook_loss else Path(sys.executable).with_name("loopx")), CODEX_BIN=binary, + MODEL_NAME="fixture", REASONING_EFFORT="high", OPENAI_BASE_URL=url, + OPENAI_API_KEY="synthetic-loopback-only", LOOPX_CODEX_TURN_TIMEOUT_SEC="90") + if checkpoint_handoff: + from contextlib import contextmanager + from benchmark.runtime import checkpoint_wake + original_enter = checkpoint_wake.CheckpointWake.__enter__ + original_hold = checkpoint_wake.held_quota_decision + + def enter(wake): + value = original_enter(wake) + assert value.listener is not None and value.socket_path + checkpoint_wakes.append(value) + return value + + @contextmanager + def held(*args, **kwargs): + with original_hold(*args, **kwargs) as capture: + # The checkpoint owner sees the original admitted + # decision before projection; it releases those bytes. + raw = capture["path"].read_bytes() + assert hashlib.sha256(raw).hexdigest() == capture["sha256"] + assert json.loads(raw)["ok"] is True + assert json.loads(raw).get("schema_version") != "context_delivery_v1" + held_captures.append((capture["path"], raw)) + yield capture + assert capture["path"].read_bytes() == raw + + monkeypatch.setattr(checkpoint_wake.CheckpointWake, "__enter__", enter) + monkeypatch.setattr(checkpoint_wake, "held_quota_decision", held) + declaration = workspace / "checkpoint-request.json" + declaration.write_text(json.dumps({"trigger": "fresh_replan", + "destination": str(workspace / "checkpoints"), + "components": [{"id": "task", "source_path": str(state)}], "point_metadata": {}})) + env["LOOPX_CHECKPOINT_REQUEST"] = str(declaration) + result = run_once(env) + assert result["ok"] and result["host_invoked"], result + assert result["context_transport"] == {"available": True} + assert result["session"]["session_id"] + if resumed: + session_id = result["session"]["session_id"] + # A later worker wake owns a new Turn. Settle the independently + # checked first readback through the real CLI, rather than + # reusing a stale guard/Turn from resumed history to obtain an + # artificially eligible warm packet. + from test_quota_settlement_cli import _run_cli + expected = hashlib.sha256(read_paths["cold"].read_text().rstrip("\n").encode()).hexdigest() + assert stages["cold_readback_sha256"] == expected + artifact = project / "native-context-readback.txt" + artifact.write_text(stages["cold_readback_sha256"]) + assert artifact.read_text() == expected + cold_turn = json.loads(snapshots[0][1])["heartbeat_receipt"]["turn_instance_id"] + binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", cold_turn) + rc, written = _run_cli(registry, runtime, "refresh-state", *binding, + "--classification", "native_context_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 + result = run_once(env) + assert result["ok"] and result["host_invoked"], result + assert result["context_transport"] == {"available": True} + assert result["session"]["session_id"] == session_id + assert not unrelated.exists() # only exact trial-owned hooks received trust + if checkpoint_handoff: + assert len(held_captures) == 2 + 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") + if resumed: + assert _app_server(binary, env, workspace, home, True, sandbox="workspace-write", + resume=session_id) == session_id + assert not errors, errors + + assert stages["phase"] == "done" + assert len(snapshots) == 2 # compaction/readback never invokes a third guard + 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 + assert len(calls) == len(observed) and stages["baseline_confirmed"] + 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 + 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")}, + capture_id=wires["warm"]["target"]["capture_id"], surface=surface) + assert restore_context_delivery(wires["warm"], baseline=wires["cold"], current=current)["context"] == current["context"] + for document in (wires["cold"]["context"], current["context"]): + context = (document["work_context"] if surface == "turn_envelope" else + document["interaction_contract"]["agent_channel"]["work_context"]) + selected = next(source["content"]["todo"] for source in context["sources"] + if source["source"] == "selected_todo") + assert selected["todo_id"] == TODO_ID and selected["text"] == body + assert all(path.read_bytes() == raw for path, raw in snapshots) + if host == "benchmark" and resumed: + assert _spend_run_count(runtime) == 1 + 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) + assert retained["text"] == body and retained["done"] is False + else: + assert state.read_bytes() == original_state and _spend_run_count(runtime) == 0 + + +@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")) +]) +def test_native_missing_hook_recovers_from_persistent_cli_route(tmp_path, monkeypatch, host, surface, hook_loss): + test_native_cli_capture_delta_and_compact_recovery(tmp_path, monkeypatch, + host, surface, False, True, True, hook_loss) + + +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +def test_native_shared_configuration_resume_and_lost_hook(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) + + +@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, + "benchmark", surface, False, True, True, "missing", checkpoint_handoff=True) + + +@pytest.mark.parametrize("private_handoff", [True], indirect=True) +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +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) diff --git a/tests/control_plane/test_codex_context_lifecycle.py b/tests/control_plane/test_codex_context_lifecycle.py new file mode 100644 index 0000000000..c0f9bfc610 --- /dev/null +++ b/tests/control_plane/test_codex_context_lifecycle.py @@ -0,0 +1,366 @@ +"""Native lifecycle references cannot stand in for retained model input.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + +from loopx.control_plane.turn_driver import codex_context as context + + +def test_shared_root_keeps_interleaved_native_sessions_independent(tmp_path, monkeypatch): + tmp_path.chmod(0o700) + monkeypatch.chdir(tmp_path) + original = {} + for session in ("one", "../../two"): + directory = Path(context.codex_context_directory(root=str(tmp_path), session=session, create=True)) + assert directory.parent == tmp_path and directory.stat().st_mode & 0o077 == 0 + monkeypatch.setenv("CODEX_THREAD_ID", session) + event = dict(session_id=session, cwd=str(tmp_path), hook_event_name="SessionStart", source="startup") + context.codex_context_hook(str(directory), event) + _, wire = project(directory) + original[session] = (directory, wire["recovery"], (directory / "session.json").read_bytes()) + assert original["one"][0] != original["../../two"][0] + for session, (directory, recovery, raw) in original.items(): + monkeypatch.setenv("CODEX_THREAD_ID", session) + selected = context.codex_context_directory(root=str(tmp_path)) + assert selected == str(directory) + assert context.read_codex_context_reference(selected, goal_id="fixture", agent_id="agent") == ( + recovery["path"], recovery["sha256"]) + assert (directory / "session.json").read_bytes() == raw + + +def test_shared_root_read_does_not_create_unknown_session_or_follow_alias(tmp_path, monkeypatch): + tmp_path.chmod(0o700) + monkeypatch.setenv("CODEX_THREAD_ID", "unknown") + with pytest.raises(FileNotFoundError): + context.codex_context_directory(root=str(tmp_path)) + assert list(tmp_path.iterdir()) == [] + monkeypatch.delenv("CODEX_THREAD_ID", raising=False) + with pytest.raises(ValueError, match="native session"): + context.codex_context_directory(root=str(tmp_path), create=True) + import hashlib + + alias = tmp_path / hashlib.sha256(b"session").hexdigest() + target = tmp_path / "target" + target.mkdir(mode=0o700) + alias.symlink_to(target, target_is_directory=True) + with pytest.raises(ValueError, match="private"): + context.codex_context_directory(root=str(tmp_path), session="session", create=True) + assert not (target / "session.json").exists() + + +def test_native_configuration_cli_is_read_only_and_usable_without_registry(tmp_path): + tmp_path.chmod(0o700) + repo = Path(__file__).resolve().parents[2] + command = [sys.executable, "-m", "loopx.entrypoint", "--format", "json", "quota", "context-config", + "--context-delivery", "on", "--codex-app", "--codex-context-root", str(tmp_path), "--goal-id", "fixture", "--agent-id", "agent"] + result = subprocess.run(command, cwd=tmp_path, env={**os.environ, "PYTHONPATH": str(repo)}, + capture_output=True, text=True, timeout=15) + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + native = payload["native_config"] + assert set(native["hooks"]) == {"SessionStart", "PreToolUse"} + 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()) == [] + missing_identity = subprocess.run(command[:-2], cwd=tmp_path, + env={**os.environ, "PYTHONPATH": str(repo)}, capture_output=True, text=True, timeout=15) + assert missing_identity.returncode == 1 and not json.loads(missing_identity.stdout)["ok"] + + +def setup(root, monkeypatch): + root.chmod(0o700) + monkeypatch.chdir(root) + monkeypatch.setenv("CODEX_THREAD_ID", "session") + monkeypatch.delenv("CODEX_ROLLOUT_TRACE_ROOT", raising=False) + event = dict(session_id="session", cwd=str(root), hook_event_name="SessionStart", source="startup") + assert context.codex_context_hook(str(root), event) == {} + return event + + +def project(root, capture_id="one", *, payload=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) + return payload, result + + +@pytest.mark.parametrize("event_name,source", [("SessionStart", "compact"), + ("SessionStart", "startup"), ("PreCompact", 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) + assert first["mode"] == "full" and first["context"] == payload + before = context._state(tmp_path) + result = context.codex_context_hook(str(tmp_path), {**event, "hook_event_name": event_name, "source": source}) + after = context._state(tmp_path) + assert before["context_epoch"] != after["context_epoch"] and "candidate" not in after + assert after["recovery"] == before["recovery"] + if event_name == "SessionStart": + body = result["hookSpecificOutput"]["additionalContext"] + assert first["recovery"]["sha256"] in body and "quota context-read" in body + assert payload["requirements"] not in body and len(body) < 2000 + else: + assert result == {} + _, second = project(tmp_path, "two") + 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): + 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") + assert second["mode"] == "full" + + +@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 + + 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}} + _, 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"}) + _, second = project(tmp_path, "two", payload=payload) + assert second["mode"] == ("delta" if case == "closed_old" else "full") + + +def test_missing_observer_or_hook_never_reuses_candidate_and_other_sessions_do_not_borrow(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) + + +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) + 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 + # A primary store failure during publication returns the whole admitted + # payload. It cannot leak an untracked candidate delta. + payload, full = project(tmp_path, "two") + assert full == payload + + +@pytest.mark.parametrize("damage", ["missing", "corrupt", "symlink", "public", "other_workspace"]) +def test_invalid_journal_falls_back_to_original_full(tmp_path, monkeypatch, damage): + setup(tmp_path, monkeypatch) + path = tmp_path / "session.json" + if damage == "missing": + path.unlink() + elif damage == "corrupt": + path.write_text("{partial") + elif damage == "symlink": + other = tmp_path / "other.json" + path.rename(other) + path.symlink_to(other) + elif damage == "public": + path.chmod(0o644) + elif damage == "other_workspace": + state = context._state(tmp_path) + state["cwd"] = str(tmp_path.parent) + context._save(tmp_path, state) + payload, full = project(tmp_path) + assert full == payload + + +def test_real_hook_cli_is_transport_only_without_registry_or_installation(tmp_path): + directory = tmp_path / "native" + directory.mkdir(mode=0o700) + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + cmd = [sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry), "--runtime-root", str(runtime), + "quota", "context-hook", "--context-delivery", "on", "--codex-app", "--codex-context-dir", str(directory)] + event = dict(session_id="session", cwd=str(tmp_path), hook_event_name="SessionStart", source="startup") + result = subprocess.run(cmd, input=json.dumps(event), capture_output=True, text=True, timeout=20, + cwd=Path(__file__).resolve().parents[2]) + assert result.returncode == 0 and json.loads(result.stdout) == {}, result.stderr + assert context._state(directory)["session_id"] == "session" + 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 loopx.cli_commands.quota_capture import read_context_capture + + setup(tmp_path, monkeypatch) + _, first = project(tmp_path, payload={"goal_id": "fixture", "requirements": "保留🙂" * 400}) + journal = tmp_path / "session.json" + args = argparse.Namespace(codex_context_dir=str(tmp_path), goal_id="fixture", agent_id="agent", + offset=0, page_bytes=256) + if damage == "session": + monkeypatch.setenv("CODEX_THREAD_ID", "other") + elif damage == "workspace": + monkeypatch.chdir(tmp_path.parent) + elif damage in {"goal", "agent"}: + setattr(args, damage + "_id", "other") + elif damage == "missing_scope": + state = context._state(tmp_path) + state["recovery"].pop("goal_id") + context._save(tmp_path, state) + elif damage == "missing": + journal.unlink() + elif damage == "corrupt": + journal.write_text("{broken") + elif damage == "public": + journal.chmod(0o644) + elif damage == "symlink": + original = tmp_path / "original.json" + journal.rename(original) + journal.symlink_to(original) + before = journal.read_bytes() if journal.exists() else None + page = read_context_capture(args) + assert (journal.read_bytes() if journal.exists() else None) == before + assert page["ok"] is (damage is None) + if damage: + assert "content" not in page + return + assert page["sha256"] == first["recovery"]["sha256"] and page["offset"] == 0 + assert "--codex-context-dir" not in page["next_command"] + assert first["recovery"]["sha256"] in page["next_command"] + # Publication can move the selector; the first page's explicit next command + # must continue the original capture, never silently switch to the new one. + _, second = project(tmp_path, "two", payload={"goal_id": "fixture", "requirements": "changed " * 400}) + assert second["recovery"]["sha256"] != page["sha256"] + args.capture_file = first["recovery"]["path"] + args.capture_sha256 = first["recovery"]["sha256"] + args.codex_context_dir = None + args.offset = page["next_offset"] + rest = read_context_capture(args) + assert rest["ok"] and rest["sha256"] == page["sha256"] + assert rest["content"] == Path(args.capture_file).read_bytes()[args.offset:args.offset + len(rest["content"].encode())].decode() + args.codex_context_dir = str(tmp_path) + assert not read_context_capture(args)["ok"] # no mixed moving and pinned selectors + + + +@pytest.mark.parametrize("setting", [None, "off", "on"]) +@pytest.mark.parametrize("host_args,expected", [ + ([], None), + (["--codex-app"], "codex_app_heartbeat"), + (["--runtime-profile", "codex_app_heartbeat"], "codex_app_heartbeat"), + (["--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), + (["--runtime-profile", "claude_code"], None), + (["--runtime-profile", "codex_cli"], None), + (["--host-surface", "codex_app"], None), + (["--codex-app", "--host-surface", "codex_cli"], None), + (["--codex-app", "--runtime-profile", "generic_cli"], None), + (["--codex-app", "--trae_app"], None), +]) +def test_activation_reuses_declared_host_mode_and_off_is_inert(tmp_path, monkeypatch, setting, host_args, expected): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import handle_native_context_command + + # Ambient native environment is not an independent activation or mode proof. + monkeypatch.setenv("CODEX_THREAD_ID", "ambient-native-session") + monkeypatch.delenv("LOOPX_EXECUTION_MODE", raising=False) + monkeypatch.delenv("LOOPX_CONTEXT_DELIVERY", raising=False) + tmp_path.chmod(0o700) + args = ["--format", "json", "quota", "context-config", "--codex-context-dir", str(tmp_path), + "--goal-id", "fixture", "--agent-id", "agent", *host_args] + if setting is not None: + args += ["--context-delivery", setting] + results = [] + parsed = build_parser().parse_args(args) + code = handle_native_context_command(parsed, print_payload=lambda value, *_: results.append(value)) + result = results[0] + if setting == "on" and expected is None: + assert code == 1 and result["ok"] is False + assert "supported declared run mode" in result["reason"] + assert "--context-delivery off" in result["reason"] + assert "native_config" not in result and list(tmp_path.iterdir()) == [] + return + assert code == 0 and result["ok"] is True + activation = result["context_delivery"] + 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"] + if expected is None: + assert "native_config" not in result + else: + for entries in result["native_config"]["hooks"].values(): + assert "--context-delivery on" in entries[0]["hooks"][0]["command"] + assert list(tmp_path.iterdir()) == [] + + +@pytest.mark.parametrize("setting", ["true", "generic_cli", "claude_code", "all", "codex_app_heartbeat"]) +def test_context_activation_rejects_non_binary_settings(setting): + from loopx.cli import build_parser + + with pytest.raises(SystemExit): + build_parser().parse_args(["quota", "context-config", "--context-delivery", setting]) + + +@pytest.mark.parametrize("mode,setting,expected", [("heartbeat", "on", True), ("plain", "on", False), + (None, "on", False), ("heartbeat", "off", False)]) +def test_benchmark_activation_uses_existing_explicit_worker_declaration(monkeypatch, mode, setting, expected): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import native_context_activation + + monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "on") + if mode is not None: + monkeypatch.setenv("LOOPX_EXECUTION_MODE", mode) + else: + monkeypatch.delenv("LOOPX_EXECUTION_MODE", raising=False) + args = build_parser().parse_args(["quota", "context-config", "--runtime-profile", "generic_cli", + "--context-delivery", setting]) + if setting == "on" and not expected: + with pytest.raises(ValueError, match="supported declared run mode"): + native_context_activation(args) + return + result = native_context_activation(args) + assert result["enabled"] is expected + assert result["scenario"] == ("codex_benchmark_heartbeat" if expected else None) diff --git a/tests/control_plane/test_codex_input_retention.py b/tests/control_plane/test_codex_input_retention.py new file mode 100644 index 0000000000..c84b34747b --- /dev/null +++ b/tests/control_plane/test_codex_input_retention.py @@ -0,0 +1,231 @@ +"""Native input observation is narrower than history, release or a future ACK.""" +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import pytest + +from loopx.control_plane.turn_driver.codex_input_trace import codex_context_in_active_input + + +def fixture(root): + bundle = root / "trace" + (bundle / "payloads").mkdir(parents=True) + baseline = {"schema_version": "context_delivery_v1", "mode": "full", + "target": {"session_id": "session"}, "context": {"enabled": True, "text": "完整🙂 " * 200, "stop": "Stop before publication."}} + raw = json.dumps(baseline, ensure_ascii=False).encode() + path = root / "capture.json" + path.write_bytes(raw) + digest = hashlib.sha256(raw).hexdigest() + # Split at ASCII JSON separators, independently from the production reader. + first = raw.index(b'"context"') + second = raw.index(b'"stop"') + pages = [] + for start, end in ((0, first), (first, second), (second, len(raw))): + page = {"ok": True, "schema_version": "context_capture_page_v1", "sha256": digest, + "offset": start, "next_offset": end if end < len(raw) else None, "total_bytes": len(raw), + "eof": end == len(raw), "content": raw[start:end].decode()} + pages.append({"type": "function_call_output", "output": "Chunk ID: fixture\nOutput:\n" + json.dumps(page)}) + request = {"input": pages} + (bundle / "manifest.json").write_text(json.dumps({"schema_version": 1, "root_thread_id": "session", "rollout_id": "session"})) + (bundle / "payloads/input.json").write_text(json.dumps(request)) + (bundle / "payloads/output.json").write_text(json.dumps({"output_items": [ + {"type": "function_call", "name": "exec_command", "call_id": "guard"}]})) + payloads = [ + {"type": "inference_started", "inference_call_id": "inference", "request_payload": {"path": "payloads/input.json"}}, + {"type": "inference_completed", "inference_call_id": "inference", "response_payload": {"path": "payloads/output.json"}}, + {"type": "tool_call_started", "tool_call_id": "guard", "model_visible_call_id": "guard", + "requester": {"type": "model"}, "kind": {"type": "exec_command"}}, + ] + rows = [{"schema_version": 1, "seq": i, "thread_id": "session", "rollout_id": "session", + "codex_turn_id": "turn", "payload": payload} for i, payload in enumerate(payloads)] + (bundle / "trace.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows)) + observation = {"trace_bundle": str(bundle), "tool_call_id": "guard", "capture_file": str(path), "capture_sha256": digest} + return observation, baseline, request, rows + + +def test_only_complete_tool_output_in_current_input_is_observed_without_writes(tmp_path): + observation, baseline, request, _ = fixture(tmp_path) + original = {path: path.read_bytes() for path in tmp_path.rglob("*") if path.is_file()} + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") + assert all(path.read_bytes() == raw for path, raw in original.items()) + # Identical repeated pages are ordinary history; every byte is still needed. + request["input"].append(request["input"][0]) + path = tmp_path / "trace/payloads/input.json" + path.write_text(json.dumps(request)) + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") + path.write_text(json.dumps({"input": [{"type": "function_call_output", "output": json.dumps(baseline)}]})) + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") + + +def test_long_trace_uses_current_window_and_never_old_delivery(tmp_path): + from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES + + observation, baseline, _, rows = fixture(tmp_path) + trace = tmp_path / "trace/trace.jsonl" + # Old history need not fit in memory or be reread to observe a live call. + old = {**rows[0], "seq": -1, "payload": {"type": "protocol_event_observed", "text": "x" * (MAX_REQUEST_BYTES + 100)}} + trace.write_text(json.dumps(old) + "\n" + trace.read_text()) + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") + # If the current inference itself fell outside the bounded window, decline. + with trace.open("a") as stream: + stream.write(json.dumps({**old, "seq": 3}) + "\n") + assert not codex_context_in_active_input(observation, baseline=baseline, session_id="session") + + +@pytest.mark.parametrize("code_mode", [False, True]) +def test_smaller_complete_rereads_recover_after_valid_json_preview(tmp_path, code_mode): + observation, baseline, request, _ = fixture(tmp_path) + raw = Path(observation["capture_file"]).read_bytes() + middle = json.loads(request["input"][1]["output"].split("\nOutput:\n")[1]) + # Host truncation inside a string can preserve valid JSON and its old cursor. + preview = {**middle, "content": middle["content"][:30] + "…9 tokens truncated…"} + request["input"][1]["output"] = json.dumps(preview) + path = tmp_path / "trace/payloads/input.json" + path.write_text(json.dumps(request)) + assert not codex_context_in_active_input(observation, baseline=baseline, session_id="session") + # Repage the same capture at different boundaries, overlapping intact old + # ranges; no new capture, fake ACK, history clearing or new admission. + split = raw.index("🙂".encode()) + recovered = [] + for start, end in ((0, split), (split, len(raw))): + page = {**middle, "offset": start, "next_offset": end if end < len(raw) else None, + "eof": end == len(raw), "content": raw[start:end].decode()} + recovered.append({"type": "function_call_output", "output": json.dumps(page)}) + request["input"].extend(recovered) + if code_mode: + # One code-mode cell can emit several raw stdout blocks. Every block, + # including the old preview, is actual model-facing input. + request["input"] = [{"type": "custom_tool_call_output", "output": [ + {"type": "input_text", "text": item["output"]} for item in request["input"]]}] + path.write_text(json.dumps(request)) + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") + # The bad preview never covers the gap when a corrected range is absent. + if code_mode: + request["input"][0]["output"].pop() + else: + request["input"].pop() + path.write_text(json.dumps(request)) + assert not codex_context_in_active_input(observation, baseline=baseline, session_id="session") + + +def test_full_projection_read_is_retention_evidence_with_json_number_semantics(tmp_path): + observation, baseline, _, _ = fixture(tmp_path) + baseline["context"]["compute"] = 1 + context = {**baseline["context"], "compute": 1.0} + raw = json.dumps(context).encode() + Path(observation["capture_file"]).write_bytes(raw) + observation["capture_sha256"] = hashlib.sha256(raw).hexdigest() + path = tmp_path / "trace/payloads/input.json" + path.write_text(json.dumps({"input": [{"type": "function_call_output", "output": raw.decode()}]})) + assert codex_context_in_active_input(observation, baseline=baseline, session_id="session") + context["compute"] = True + path.write_text(json.dumps({"input": [{"type": "function_call_output", "output": json.dumps(context)}]})) + assert not codex_context_in_active_input(observation, baseline=baseline, session_id="session") + + +@pytest.mark.parametrize("failure", ["middle_missing", "pruned", "preview", "quoted", "wrong_role", "server_history", + "bad_page", "shifted_offset", "capture_changed", "other_session", "other_tool", "ended", "new_inference", "compacted", + "child", "code_mode", "wrong_response", "partial_trace", "reordered_trace", "missing_trace", "outside_payload", "symlink", "boolean_type"]) +def test_unknown_stale_lossy_or_unbound_observation_never_reuses(tmp_path, failure): + observation, baseline, request, rows = fixture(tmp_path) + bundle = Path(observation["trace_bundle"]) + if failure == "middle_missing": + request["input"].pop(1) + elif failure == "pruned": + request["input"] = [] + elif failure == "preview": + request["input"][1]["output"] = "Warning: truncated output\n" + request["input"][1]["output"][:100] + elif failure == "quoted": + request["input"] = [{"type": "function_call_output", "output": "Example: " + json.dumps(baseline)}] + elif failure == "wrong_role": + request["input"] = [{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": json.dumps(baseline)}]}] + elif failure == "boolean_type": + other = json.loads(json.dumps(baseline)) + other["context"]["enabled"] = 1 + request["input"] = [{"type": "function_call_output", "output": json.dumps(other)}] + elif failure == "server_history": + request["previous_response_id"] = "opaque-history" + elif failure == "bad_page": + request["input"][1]["output"] = request["input"][1]["output"].replace('"eof": false', '"eof": true') + elif failure == "shifted_offset": + page = json.loads(request["input"][1]["output"].split("\nOutput:\n")[1]) + page["offset"] -= 1 + request["input"][1]["output"] = json.dumps(page) + elif failure == "capture_changed": + Path(observation["capture_file"]).write_text("{}") + elif failure == "other_session": + baseline["target"]["session_id"] = "other" + elif failure == "other_tool": + observation["tool_call_id"] = "other" + elif failure == "ended": + rows.append({**rows[-1], "seq": 3, "payload": {"type": "tool_call_ended", "tool_call_id": "guard"}}) + elif failure in {"new_inference", "compacted"}: + rows.append({**rows[0], "seq": 3, "payload": {**rows[0]["payload"], "inference_call_id": "later"}}) + elif failure == "child": + rows[-1]["thread_id"] = "child" + elif failure == "code_mode": + rows[-1]["payload"]["code_mode_runtime_tool_id"] = "nested" + elif failure == "wrong_response": + (bundle / "payloads/output.json").write_text(json.dumps({"output_items": []})) + elif failure == "reordered_trace": + rows[0], rows[1] = rows[1], rows[0] + elif failure == "outside_payload": + rows[0]["payload"]["request_payload"]["path"] = "../input.json" + (bundle / "payloads/input.json").write_text(json.dumps(request)) + trace = bundle / "trace.jsonl" + trace.write_text("".join(json.dumps(row) + "\n" for row in rows)) + if failure == "partial_trace": + trace.write_text(trace.read_text() + '{"partial":') + elif failure == "missing_trace": + trace.unlink() + elif failure == "symlink": + original = tmp_path / "original.jsonl" + trace.rename(original) + trace.symlink_to(original) + assert not codex_context_in_active_input(observation, baseline=baseline, session_id="session") + + +@pytest.mark.parametrize("failure", [None, "parent_missing", "parent_changed", "parent_ended", "parent_wrong_call", + "child_wrong_cell", "child_wrong_tool", "output_missing", "output_preview", "output_wrong_role"]) +def test_code_mode_requires_exact_live_parent_and_complete_model_visible_output(tmp_path, failure): + observation, baseline, request, rows = fixture(tmp_path) + bundle = Path(observation["trace_bundle"]) + source = 'text(await tools.exec_command({cmd:"loopx quota should-run"}));' + output = {"output_items": [{"type": "custom_tool_call", "name": "exec", "call_id": "parent", "input": source}]} + parent = {**rows[-1], "seq": 2, "payload": {"type": "code_cell_started", "runtime_cell_id": "cell", + "model_visible_call_id": "parent", "source_js": source}} + rows[-1]["seq"] = 3 + rows[-1]["payload"].update(requester={"type": "code_cell", "runtime_cell_id": "cell"}, + model_visible_call_id=None, code_mode_runtime_tool_id="tool-1") + rows.insert(2, parent) + request["input"] = [{"type": "custom_tool_call_output", "call_id": f"read-{i}", "output": [ + {"type": "input_text", "text": "Script completed\nOutput:\n"}, + {"type": "input_text", "text": json.dumps({"chunk_id": "fixture", "exit_code": 0, + "output": page["output"].split("\nOutput:\n")[1]})}]} for i, page in enumerate(request["input"])] + if failure == "parent_missing": + rows.remove(parent) + elif failure == "parent_changed": + parent["payload"]["source_js"] += "text('different');" + elif failure == "parent_ended": + rows.append({**parent, "seq": 4, "payload": {"type": "code_cell_ended", "runtime_cell_id": "cell"}}) + elif failure == "parent_wrong_call": + output["output_items"][0]["call_id"] = "different" + elif failure == "child_wrong_cell": + rows[-1]["payload"]["requester"]["runtime_cell_id"] = "other-cell" + elif failure == "child_wrong_tool": + rows[-1]["payload"]["kind"] = {"type": "other"} + elif failure == "output_missing": + request["input"].pop(1) + elif failure == "output_preview": + request["input"][1]["output"][1]["text"] = request["input"][1]["output"][1]["text"][:200] + elif failure == "output_wrong_role": + for item in request["input"]: + item["type"] = "message" + item["role"] = "assistant" + (bundle / "payloads/output.json").write_text(json.dumps(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) diff --git a/tests/control_plane/test_context_capture_read.py b/tests/control_plane/test_context_capture_read.py new file mode 100644 index 0000000000..d529ee0b71 --- /dev/null +++ b/tests/control_plane/test_context_capture_read.py @@ -0,0 +1,113 @@ +"""Original-capture paging never consults current authority or reruns a guard.""" +import argparse +import hashlib +import json +import os +import shlex + +import pytest + +from loopx.cli_commands.quota_capture import read_context_capture +from test_quota_settlement_cli import _run_cli + + +def _args(path, raw, **changes): + return argparse.Namespace(capture_file=str(path), capture_sha256=hashlib.sha256(raw).hexdigest(), + **{"offset": 0, "page_bytes": 256, **changes}) + + +def test_cli_pages_reconstruct_unicode_capture_without_goal_or_runtime(tmp_path): + path = tmp_path / "capture with ' quote.json" + original = {"requirements": ("原始要求🙂\n保留权限、停止条件与证据。" * 70), "late": "stop now", "null": None} + raw = (json.dumps(original, ensure_ascii=False, separators=(",", ":")) + "\n").encode() + path.write_bytes(raw) + runtime, registry = tmp_path / "absent-runtime", tmp_path / "absent-registry.json" + args = ["quota", "context-read", "--capture-file", str(path), "--capture-sha256", + hashlib.sha256(raw).hexdigest(), "--page-bytes", "256"] + chunks, offset = [], 0 + while True: + code, page = _run_cli(registry, runtime, *args, cwd=tmp_path) + assert code == 0 and page["ok"], page + assert page["offset"] == offset and page["total_bytes"] == len(raw) + chunk = page["content"].encode() + assert 0 < len(chunk) <= 256 + assert chunk == raw[offset:offset + len(chunk)] + chunks.append(chunk) + offset += len(chunk) + if page["eof"]: + assert page["next_offset"] is None and "next_command" not in page + break + assert page["next_offset"] == offset + command = shlex.split(page["next_command"]) + assert command[:3] == ["loopx", "--format", "json"] + args = command[3:] + assert b"".join(chunks) == raw and json.loads(b"".join(chunks)) == original + assert path.read_bytes() == raw and not runtime.exists() and not registry.exists() + + +@pytest.mark.parametrize("kind", ["digest", "changed_between_pages", "missing", "symlink", "directory", + "not_json", "not_object", "not_utf8", "offset", "utf8_offset", "small_page", "large_page"]) +def test_invalid_capture_never_returns_partial_context(tmp_path, kind): + path = tmp_path / "projection.json" + raw = json.dumps({"source": "原始🙂" * 100}, ensure_ascii=False).encode() + path.write_bytes(raw) + args = _args(path, raw) + if kind == "digest": + args.capture_sha256 = "0" * 64 + elif kind == "changed_between_pages": + first = read_context_capture(args) + assert first["ok"] and not first["eof"] + args.offset = first["next_offset"] + path.write_bytes(raw.replace(b"source", b"target")) + elif kind == "missing": + path.unlink() + elif kind == "symlink": + target = tmp_path / "original.json" + path.rename(target) + path.symlink_to(target) + elif kind == "directory": + path.unlink() + path.mkdir() + elif kind in {"not_json", "not_object", "not_utf8"}: + raw = {"not_json": b'{"missing":', "not_object": b'[]', "not_utf8": b'{"x":"\xff"}'}[kind] + path.write_bytes(raw) + args.capture_sha256 = hashlib.sha256(raw).hexdigest() + elif kind == "offset": + args.offset = len(raw) + 1 + elif kind == "utf8_offset": + args.offset = raw.index("原".encode()) + 1 + else: + args.page_bytes = 255 if kind == "small_page" else 8193 + result = read_context_capture(args) + assert result["ok"] is False and "content" not in result and "next_command" not in result + + +@pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="POSIX named pipe") +def test_capture_reader_rejects_fifo_without_waiting_for_a_writer(tmp_path): + path = tmp_path / "projection.json" + os.mkfifo(path) + assert read_context_capture(_args(path, b"{}"))["ok"] is False + + +def test_capture_reader_accepts_parent_directory_alias(tmp_path): + original = tmp_path / "original" + original.mkdir() + alias = tmp_path / "alias" + alias.symlink_to(original, target_is_directory=True) + raw = b'{"requirement":"preserve the complete original capture"}' + (original / "projection.json").write_bytes(raw) + result = read_context_capture(_args(alias / "projection.json", raw)) + assert result["ok"] and result["eof"] and result["content"].encode() == raw + + +def test_recovery_page_keeps_invocation_executable_and_resets_afterwards(tmp_path): + from loopx.command_invocation import loopx_cli_route + path = tmp_path / "projection.json" + raw = json.dumps({"requirements": "preserve " * 100}).encode() + path.write_bytes(raw) + executable = str(tmp_path / "pinned release" / "loopx") + with loopx_cli_route(executable): + page = read_context_capture(_args(path, raw)) + assert shlex.split(page["next_command"])[:3] == [executable, "--cli-bin", executable] + plain = read_context_capture(_args(path, raw)) + assert shlex.split(plain["next_command"])[:3] == ["loopx", "--format", "json"] diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py new file mode 100644 index 0000000000..9965ced4d8 --- /dev/null +++ b/tests/control_plane/test_context_delivery_cli.py @@ -0,0 +1,344 @@ +"""Real quota sources share an exact, effect-free full/delta presentation owner. + +These tests qualify the CLI and RPC, not native Host retention observation. +""" +from __future__ import annotations + +from copy import deepcopy +import json +import os +from pathlib import Path + +import pytest + +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 ( + build_turn_envelope, prepare_context_delivery, restore_context_delivery, +) +from test_quota_settlement_cli import ( + AGENT_ID, GOAL_ID, TODO_ID, + _heartbeat_receipt_count, _run_cli, _spend_run_count, _write_fixture, +) + + +@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): + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + body = "Preserve every source row and cancellation condition. " * 500 + body += "FINAL CONDITION: stop before publication." + todos = read_fixture_todos(state) + todos[0].update(text=body, title=body) + 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) + before_source = state.read_bytes() + host = ["--codex-app"] if profile == "codex_app" else ["--runtime-profile", profile] + rc, guard = _run_cli(registry, runtime, "quota", "should-run", *host, + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, + "--turn-instance-id", "context-presentation", cwd=project) + assert rc == 0 and guard["should_run"] is True, guard + context = guard["interaction_contract"]["agent_channel"]["work_context"] + selected = next(s["content"]["todo"] for s in context["sources"] if s.get("source") == "selected_todo") + assert selected["text"] == body + assert selected.get("title") != body # only the exact duplicate is elided + turn_context = build_turn_envelope(guard)["work_context"] + assert turn_context["sources"] == context["sources"] + assert turn_context["instruction"] == context["instruction"] + original = deepcopy(guard) + scope = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id=profile, context_epoch="epoch-a") + full = prepare_context_delivery(context, scope=scope, capture_id="capture-a") + assert full["mode"] == "full" and full["context"] == context + warm = prepare_context_delivery(context, scope=scope, capture_id="capture-b", + baseline=full, retained={"complete": True, "target": full["target"]}) + assert warm["mode"] == "delta" + assert len(json.dumps(warm).encode()) < len(json.dumps(full).encode()) / 2 + current = prepare_context_delivery(context, scope=scope, capture_id="capture-b") + assert restore_context_delivery(warm, baseline=full, current=current)["context"] == context + turn_full = prepare_context_delivery(turn_context, scope=scope, capture_id="turn-capture") + turn_delta = prepare_context_delivery(turn_context, scope=scope, capture_id="turn-capture", + baseline=full, retained={"complete": True, "target": full["target"]}) + assert turn_delta["mode"] == "delta" + assert restore_context_delivery(turn_delta, baseline=full, current=turn_full)["context"] == turn_context + for baseline, delivery in [(None, warm), (full, {**warm, "changes": None})]: + recovered = restore_context_delivery(delivery, baseline=baseline, current=current) + assert recovered["mode"] == "full" and recovered["context"] == context + assert recovered["target"] == current["target"] + reset = prepare_context_delivery(context, scope={**scope, "context_epoch": "epoch-b"}, + capture_id="capture-b", baseline=full, retained={"complete": True, "target": full["target"]}) + assert reset["mode"] == "full" and reset["context"] == context + # The same RPC now transports the whole ordinary quota packet and whole + # Turn envelope, rather than assuming the common work_context is the packet. + for surface, projection in [("quota_packet", guard), ("turn_envelope", build_turn_envelope(guard))]: + first = prepare_context_delivery(projection, scope=scope, capture_id="whole-a", surface=surface) + next_capture = prepare_context_delivery(projection, scope=scope, capture_id="whole-b", surface=surface) + delta = prepare_context_delivery(projection, scope=scope, capture_id="whole-b", surface=surface, + baseline=first, retained={"complete": True, "target": first["target"]}) + assert delta["mode"] == "delta" + restored = restore_context_delivery(delta, baseline=first, current=next_capture) + assert restored["mode"] == "delta" and restored["context"] == projection + missing = restore_context_delivery(delta, baseline=None, current=next_capture) + assert missing["mode"] == "full" and missing["context"] == projection + # Neither native source state, the signed decision nor settlement is changed + # by presentation, warm reuse or recovery. A new capture is not an admission. + assert state.read_bytes() == before_source + assert guard == original + assert _spend_run_count(runtime) == 0 + rc, readback = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_ID) + assert rc == 0 and readback["todo"]["text"] == body, readback + assert readback["todo"]["status"] == "open" + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"]) +@pytest.mark.skipif(os.name != "posix", reason="Unix local Host transport") +def test_real_cli_host_rendezvous_sends_delta_and_keeps_same_capture_full(tmp_path, monkeypatch, provider, surface): + """The ordinary CLI really uses the codec; the listener models a Host, + not a claim that a particular installed Codex has observed retention.""" + import hashlib + import socket + import tempfile + from concurrent.futures import ThreadPoolExecutor + from test_quota_settlement_cli import _heartbeat_receipt_count + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + body = "Retain full task evidence and permission boundaries. " * 800 + "Stop before publication." + todos = read_fixture_todos(state) + todos[0].update(text=body, title=body) + 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) + before_source = state.read_bytes() + root = tmp_path / "captures" + root.mkdir() + scope = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, session_id="host-session", context_epoch="epoch-1") + saved = [] + + def invoke(host_request, *, opt_in=True, native_context=False, setting="on"): + with tempfile.TemporaryDirectory(prefix="context-host-", dir="/tmp") as short: + endpoint = Path(short).resolve() / "host.sock" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server: + server.bind(str(endpoint)) + server.listen(1) + server.settimeout(30) + def release(): + connection, _ = server.accept() + with connection: + connection.settimeout(30) + with connection.makefile("rb") as stream: + observed = json.loads(stream.readline(4096)) + path = Path(observed["path"]) + raw = path.read_bytes() + assert hashlib.sha256(raw).hexdigest() == observed["sha256"] + if opt_in: + request = path.parent / "delivery-request.json" + request.write_text(host_request if isinstance(host_request, str) else json.dumps(host_request)) + request.chmod(0o600) + response = {"released": True, "sha256": observed["sha256"]} + if opt_in: + response["context_delivery"] = True + connection.sendall(json.dumps(response).encode() + b"\n") + return path, raw + with ThreadPoolExecutor(max_workers=1) as workers: + future = workers.submit(release) + code, wire = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, + "--turn-instance-id", "host-context-delivery", + "--context-delivery", setting, + "--decision-output-root", str(root), "--decision-handoff-socket", str(endpoint), + *(["--codex-context-dir", str(tmp_path / "unavailable-native-state")] if native_context else []), + *(["--turn-envelope"] if surface == "turn_envelope" else []), cwd=project) + path, raw = future.result(timeout=30) + assert code == 0, wire + assert path.read_bytes() == raw + saved.append((path, raw)) + return wire, path.parent + + cold, first = invoke({"scope": scope}) + assert cold["mode"] == "full" + assert json.loads((first / "projection.json").read_bytes()) == cold["context"] + warm, second = invoke({"scope": scope, "baseline": cold, + "retained": {"complete": True, "target": cold["target"]}}) + assert warm["mode"] == "delta" + projection = json.loads((second / "projection.json").read_bytes()) + current = prepare_context_delivery(projection, scope=scope, + capture_id=warm["target"]["capture_id"], surface=surface) + assert restore_context_delivery(warm, baseline=cold, current=current)["context"] == projection + assert len(json.dumps(warm)) < len(json.dumps(current)) + assert warm["recovery"]["path"] == str(second / "projection.json") + assert warm["target"]["capture_id"] == hashlib.sha256((second / "decision.json").read_bytes()).hexdigest() + assert warm["recovery"]["sha256"] == hashlib.sha256((second / "projection.json").read_bytes()).hexdigest() + # Two consumers cannot compose nested envelopes or transfer retention + # authority. The explicitly selected native consumer has no proof here. + no_native_proof, _ = invoke({"scope": scope, "baseline": cold, + "retained": {"complete": True, "target": cold["target"]}}, native_context=True) + assert "target" not in no_native_proof and "context" not in no_native_proof + assert no_native_proof["ok"] and no_native_proof.get("schema_version") != "context_delivery_v1" + before_receipts = _heartbeat_receipt_count(runtime, "host-context-delivery") + # Compaction between preparation and dispatch: the Host can select the exact + # full projection without a second CLI call, admission or state mutation. + recovered = restore_context_delivery(warm, baseline=None, current=current) + assert recovered["mode"] == "full" and recovered["context"] == projection + assert _heartbeat_receipt_count(runtime, "host-context-delivery") == before_receipts + reset, _ = invoke({"scope": {**scope, "context_epoch": "compacted"}, "baseline": cold, + "retained": {"complete": True, "target": cold["target"]}}) + assert reset["mode"] == "full" + unobserved, directory = invoke({"scope": scope, "baseline": cold, + "retained": {"complete": True, "target": cold["target"]}, "native_input": {}}) + assert unobserved["mode"] == "full" and unobserved["reason"] == "baseline_unavailable" + assert unobserved["context"] == json.loads((directory / "projection.json").read_bytes()) + for malformed in ["{truncated", "[" * 2000 + "]" * 2000, + {"scope": {**scope, "agent_id": "another-agent"}}]: + full, directory = invoke(malformed) + assert "target" not in full # Automatic ordinary full output, not a gate. + assert full == json.loads((directory / "projection.json").read_bytes()) + # A host-side request cannot enable this feature when the caller is off. + disabled, disabled_directory = invoke({"scope": scope, "baseline": cold, + "retained": {"complete": True, "target": cold["target"]}}, setting="off") + assert "target" not in disabled and not (disabled_directory / "projection.json").exists() + ordinary, directory = invoke(None, opt_in=False) + assert "target" not in ordinary and not (directory / "projection.json").exists() + assert state.read_bytes() == before_source and _spend_run_count(runtime) == 0 + assert all(path.read_bytes() == raw for path, raw in saved) + + +def test_optional_capture_projection_failure_falls_back_without_hiding_source(tmp_path, monkeypatch): + from loopx.control_plane.turn_driver.decision_capture import project_captured_context + from loopx.control_plane.quota import turn_envelope + from loopx.control_plane.effect_runtime import EffectRuntimeStartupError + payload = {"ok": True, "goal_id": "fixture", "requirements": "Keep late stop conditions."} + (tmp_path / "decision.json").write_text(json.dumps(payload)) + def unavailable(*args, **kwargs): + raise EffectRuntimeStartupError("synthetic unavailable codec") + monkeypatch.setattr(turn_envelope, "prepare_context_delivery", unavailable) + assert project_captured_context(payload, directory=tmp_path, request={"scope": { + "goal_id": "fixture", "agent_id": "agent", "session_id": "session", "context_epoch": "epoch"}}, + goal_id="fixture", agent_id="agent", surface="quota_packet", capture_id="fixture-capture") is payload + assert json.loads((tmp_path / "projection.json").read_bytes()) == payload + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_native_context_cli_without_retention_stays_full_and_preserves_receipt(tmp_path, monkeypatch, provider): + from loopx.control_plane.turn_driver.codex_context import codex_context_hook + from test_quota_settlement_cli import _heartbeat_receipt_count + + isolate_sqlite_runtime(tmp_path, monkeypatch) + monkeypatch.setenv("CODEX_THREAD_ID", "native-session") + monkeypatch.delenv("CODEX_ROLLOUT_TRACE_ROOT", raising=False) + project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=read_fixture_todos(state), handoff_mode="soft_claim"), + state_path=state, provider=provider) + original = state.read_bytes() + root, captures = tmp_path / "context", tmp_path / "captures" + root.mkdir(mode=0o700) + captures.mkdir() + event = dict(session_id="native-session", cwd=str(project), hook_event_name="SessionStart", source="startup") + codex_context_hook(str(root), event) + for _ in range(2): + rc, wire = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--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"] + assert _heartbeat_receipt_count(runtime, "native-full") == 1 + assert _spend_run_count(runtime) == 0 and state.read_bytes() == original + + +@pytest.mark.parametrize("native_failure", [False, True]) +def test_native_projection_never_layers_host_projection(tmp_path, monkeypatch, native_failure): + from loopx.cli import build_parser + from loopx.cli_commands import quota + from loopx.control_plane.turn_driver import codex_context + + args = build_parser().parse_args(["--format", "json", "quota", "should-run", + "--goal-id", "fixture", "--agent-id", "worker", "--context-delivery", "on", "--codex-app", "--codex-context-dir", str(tmp_path)]) + original = {"ok": True, "goal_id": "fixture", "agent_identity": {"agent_id": "worker"}} + monkeypatch.setattr(quota, "handoff_decision", lambda *args: ({"scope": "host-request"}, "digest")) + monkeypatch.setattr(quota, "_project_quota_cli_payload", lambda payload, *args, **kwargs: payload) + + def competing(*args, **kwargs): + pytest.fail("explicit native observation must not compose two projection owners") + + def native(payload, **kwargs): + assert payload == original + if native_failure: + raise OSError("native observation unavailable") + return {"native_projection": payload} + + monkeypatch.setattr(quota, "project_captured_context", competing) + monkeypatch.setattr(codex_context, "project_codex_context", native) + printed = [] + assert quota._emit_quota_result(original, args, context=None, registry_path=tmp_path / "absent.json", + heartbeat_turn_id=None, usage_quota_started=0, capture_directory=tmp_path, + detail_sections=frozenset(), print_payload=lambda value, *args: printed.append(value)) == 0 + assert printed == [original if native_failure else {"native_projection": original}] + assert json.loads((tmp_path / "decision.json").read_text()) == original + + +@pytest.mark.parametrize("scenario", [None, "off"]) +def test_plain_quota_context_default_does_not_touch_transport(tmp_path, monkeypatch, scenario): + from loopx.control_plane.turn_driver import codex_context + from loopx.cli import build_parser + + def unexpected(*args, **kwargs): + pytest.fail("off must not observe native context or create transport state") + monkeypatch.setattr(codex_context, "project_codex_context", unexpected) + project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=read_fixture_todos(state), handoff_mode="soft_claim"), + state_path=state, provider="file") + args = ["quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "off-scenario"] + if scenario: + args += ["--context-delivery", scenario] + code, result = _run_cli(registry, runtime, *args, cwd=project) + assert code == 0 and result["ok"] and result.get("schema_version") != "context_delivery_v1", result + assert build_parser().parse_args(["quota", "should-run"]).context_delivery == "off" + + +@pytest.mark.parametrize("activation", [[], ["--context-delivery", "off"], ["--context-delivery", "on"]]) +def test_off_keeps_full_but_explicit_unsupported_on_errors_before_admission(tmp_path, monkeypatch, activation): + monkeypatch.delenv("LOOPX_CONTEXT_DELIVERY", raising=False) + monkeypatch.delenv("LOOPX_EXECUTION_MODE", raising=False) + project, runtime, registry = _write_fixture(tmp_path) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + initialize_canonical_authority(runtime, GOAL_ID, + build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=read_fixture_todos(state), handoff_mode="soft_claim"), + state_path=state, provider="file") + root = tmp_path / "never-create" + code, result = _run_cli(registry, runtime, "quota", "should-run", "--runtime-profile", "generic_cli", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "unsupported", + "--codex-context-dir", str(root), *activation, cwd=project) + assert not root.exists() + if activation == ["--context-delivery", "on"]: + assert code != 0 and result["ok"] is False, result + assert "supported declared run mode" in result["reason"] + assert _heartbeat_receipt_count(runtime, "unsupported") == 0 + assert _spend_run_count(runtime) == 0 + else: + assert code == 0 and result["ok"] and result.get("schema_version") != "context_delivery_v1", result + + +@pytest.mark.parametrize("command", ["context-config", "context-hook", "context-project", "should-run"]) +@pytest.mark.parametrize("host", [[], ["--host-surface", "codex_app"], ["--runtime-profile", "claude_code"]]) +def test_unsupported_native_commands_fail_without_creating_transport_or_authority(tmp_path, command, host): + # A missing registry/capture must not hide this explicit configuration error + # or turn it into an optional-transport full fallback (including hooks). + runtime, registry = tmp_path / "runtime", tmp_path / "missing-registry.json" + native, captures = tmp_path / "native", tmp_path / "captures" + code, result = _run_cli(registry, runtime, "quota", command, *host, "--context-delivery", "on", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "unqualified", + "--codex-context-dir", str(native), "--decision-output-root", str(captures), cwd=tmp_path) + assert code != 0 and result["ok"] is False, result + assert "supported declared run mode" in result["reason"] + 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 diff --git a/tests/control_plane/test_heartbeat_envelope_transport.py b/tests/control_plane/test_heartbeat_envelope_transport.py index c15acf591a..9e742e447d 100644 --- a/tests/control_plane/test_heartbeat_envelope_transport.py +++ b/tests/control_plane/test_heartbeat_envelope_transport.py @@ -23,6 +23,36 @@ ) +def test_shared_native_root_keeps_dynamic_turn_binding_and_bootstrap_options(tmp_path): + from loopx.cli import build_parser + from loopx.control_plane.heartbeat.bootstrap_prompt import goal_bootstrap, host_bootstrap_binding + + args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile="codex_app_heartbeat", + thin=True, decision_output_root=tmp_path, codex_context_root=str(tmp_path), context_delivery="on", turn_envelope=True) + prompt = build_heartbeat_prompt(**args) + guard = prompt["quota_guard_command"] + assert '--turn-instance-id "${LOOPX_TURN:?}"' in guard + assert "--begin-turn" not in guard + assert "--codex-context-root" in guard and "--turn-envelope" in guard + assert "LOOPX_TURN=" in prompt["task_body"] + # The shell refuses an unbound wake before the CLI can create a receipt. + result = subprocess.run(guard, shell=True, env={"PATH": os.environ["PATH"]}, + capture_output=True, text=True) + assert result.returncode != 0 and "LOOPX_TURN" in result.stderr + assert list(tmp_path.iterdir()) == [] + parser = build_parser() + cli = ["heartbeat-prompt", "--thin", "--codex-app", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--decision-output-root", str(tmp_path), "--context-delivery", "on", "--codex-app", "--codex-context-root", str(tmp_path), "--turn-envelope", "--bootstrap"] + parsed = parser.parse_args(cli) + bootstrap = goal_bootstrap(parsed, registry=tmp_path / "registry.json") + command = shlex.split(bootstrap.split("```sh\n")[1].split("\n```")[0]) + assert "--bootstrap" not in command and "--turn-instance-id" not in command + assert command[command.index("--codex-context-root") + 1] == str(tmp_path) + assert "--turn-envelope" in command + binding = host_bootstrap_binding(bootstrap) + assert binding is not None and binding["context_delivery"] == "on" and binding["turn_envelope"] is True + + def test_capture_preserves_full_dispatcher_and_envelope_needs_explicit_opt_in(tmp_path): args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile="generic_cli", turn_instance_id=TURN_ID, thin=True) @@ -186,8 +216,9 @@ def release(): @pytest.mark.parametrize('provider', ['file', 'sqlite']) @pytest.mark.parametrize('enabled', [False, True]) @pytest.mark.parametrize('private_handoff', [False, True], indirect=True) +@pytest.mark.parametrize('context_delivery', [False, True]) def test_real_worker_generated_guard_selection_reentry_and_settle_once( - tmp_path, monkeypatch, provider, enabled, private_handoff, + tmp_path, monkeypatch, provider, enabled, private_handoff, context_delivery, ): from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime @@ -209,6 +240,14 @@ def test_real_worker_generated_guard_selection_reentry_and_settle_once( LOOPX_REGISTRY=str(registry), LOOPX_RUNTIME_ROOT=str(runtime), LOOPX_PROJECT=str(project), LOOPX_GOAL_ID=GOAL_ID, LOOPX_AGENT_ID=AGENT_ID, LOOPX_WAKE_LOG_DIR=str(logs), LOOPX_TURN_ENVELOPE='1' if enabled else '0') + if context_delivery: + # No native proof in this CLI fixture: composed capture must still + # release ordinary full output and preserve reentry and settlement. + 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. + monkeypatch.setenv('LOOPX_CONTEXT_DELIVERY', 'on') + monkeypatch.setenv('LOOPX_EXECUTION_MODE', 'heartbeat') body = heartbeat_body(env, TURN_ID, native_goal=False, decision_handoff_socket=private_handoff) # Execute the renderer's actual guard, preserving its route and flags. block = body.split('```sh\n', 1)[1].split('```', 1)[0] @@ -219,7 +258,7 @@ def test_real_worker_generated_guard_selection_reentry_and_settle_once( first = json.loads(result.stdout) assert result.returncode == 0, first root = logs / TURN_ID / 'decisions' - captured = enabled or private_handoff is not None + captured = enabled or private_handoff is not None or context_delivery if captured: captures = list(root.glob('*/decision.json')) assert len(captures) == 1 @@ -322,3 +361,43 @@ def test_renderer_retains_owned_handoff_without_enabling_it_by_default(tmp_path) build_heartbeat_prompt(**args, decision_handoff_socket=endpoint) with pytest.raises(ValueError, match='invalid decision handoff socket'): build_heartbeat_prompt(**args, decision_output_root=tmp_path, decision_handoff_socket=endpoint) + + +@pytest.mark.parametrize("profile", [None, "generic_cli", "claude_code"]) +def test_native_context_prompt_rejects_unqualified_on_and_keeps_off_inert(tmp_path, monkeypatch, profile): + monkeypatch.delenv("LOOPX_EXECUTION_MODE", raising=False) + monkeypatch.delenv("LOOPX_CONTEXT_DELIVERY", raising=False) + args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile=profile, + thin=True) + assert build_heartbeat_prompt(**args) == build_heartbeat_prompt(**args, context_delivery="off") + with pytest.raises(ValueError, match="supported declared run mode"): + build_heartbeat_prompt(**args, context_delivery="on", codex_context_dir=str(tmp_path / "absent")) + assert list(tmp_path.iterdir()) == [] + + +def test_native_context_prompt_keeps_capture_on_reentry(tmp_path, monkeypatch): + monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "on") + monkeypatch.setenv("LOOPX_EXECUTION_MODE", "heartbeat") + args = dict(goal_id=GOAL_ID, agent_id=AGENT_ID, runtime_profile="generic_cli", + turn_instance_id=TURN_ID, thin=True, decision_output_root=tmp_path) + plain = build_heartbeat_prompt(**args) + context = str(tmp_path / "private context") + enabled = build_heartbeat_prompt(**args, context_delivery="on", codex_context_dir=context) + suffix = " --context-delivery on --codex-context-dir " + shlex.quote(context) + assert enabled["quota_guard_command"] == plain["quota_guard_command"] + suffix + assert suffix in enabled["thin_prompt_command"] + assert enabled["task_body"].replace(suffix, "") == plain["task_body"] + with pytest.raises(ValueError, match="capture root"): + build_heartbeat_prompt(**(args | {"decision_output_root": None}), context_delivery="on", codex_context_dir=context) + with pytest.raises(ValueError, match="invalid decision handoff socket"): + build_heartbeat_prompt(**args, context_delivery="on", codex_context_dir=context, decision_handoff_socket="unused") + with tempfile.TemporaryDirectory(prefix="context-handoff-", dir="/tmp") as short: + endpoint = str(Path(short).resolve() / "host.sock") + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(endpoint) + packet = build_heartbeat_prompt(**args, context_delivery="on", codex_context_dir=context, decision_handoff_socket=endpoint) + for field in ("quota_guard_command", "thin_prompt_command"): + argv = shlex.split(packet[field]) + 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 diff --git a/tests/control_plane/test_quota_decision_capture.py b/tests/control_plane/test_quota_decision_capture.py index 085a27a39e..993a345a72 100644 --- a/tests/control_plane/test_quota_decision_capture.py +++ b/tests/control_plane/test_quota_decision_capture.py @@ -10,6 +10,9 @@ from loopx.cli_commands.quota_capture import capture_decision, prepare_decision_capture from loopx.control_plane.quota.error_codes import QuotaCommandValidationError +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_source_fixture, read_fixture_todos, +) from tests.control_plane.test_quota_settlement_cli import ( AGENT_ID, GOAL_ID, TODO_ID, TURN_ID, _write_fixture, _configure_read_only_todo, _run_cli, _heartbeat_receipt_count, @@ -84,10 +87,8 @@ def test_real_cli_fallback_reference_keeps_capture_and_turn_effects(tmp_path, mo f' \n') - code, listed = _run_cli(registry, runtime, 'todo', 'list', '--goal-id', GOAL_ID) - assert code == 0, listed projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode='soft_claim', todos=listed['todos'], + goal_id=GOAL_ID, handoff_mode='soft_claim', todos=read_fixture_todos(state), ) initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) args = ['quota', 'should-run', '--goal-id', GOAL_ID, '--agent-id', AGENT_ID, @@ -140,10 +141,8 @@ def test_real_cli_boundary_clauses_reach_shared_host_without_loss(tmp_path, monk goal['coordination'].update(write_scope=scopes, requires_parent_approval=['publish', 'production-action']) goal['guards'] = guards registry.write_text(json.dumps(configured)) - code, listed = _run_cli(registry, runtime, 'todo', 'list', '--goal-id', GOAL_ID) - assert code == 0, listed projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode='soft_claim', todos=listed['todos'], + goal_id=GOAL_ID, handoff_mode='soft_claim', todos=read_fixture_todos(state), ) initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) args = ['quota', 'should-run', '--goal-id', GOAL_ID, '--agent-id', AGENT_ID, @@ -199,10 +198,8 @@ def test_real_guard_saved_before_envelope_and_read_without_reexecution(tmp_path, registry.write_text(json.dumps(configured)) _configure_read_only_todo(project) state = project / f'.codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md' - code, listed = _run_cli(registry, runtime, 'todo', 'list', '--goal-id', GOAL_ID) - assert code == 0, listed projection = build_todo_runtime_shadow_projection( - goal_id=GOAL_ID, handoff_mode='soft_claim', todos=listed['todos'], + goal_id=GOAL_ID, handoff_mode='soft_claim', todos=read_fixture_todos(state), ) initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider) capture = tmp_path / "guard ' $(touch INJECTED) `touch INJECTED`" @@ -314,7 +311,8 @@ def test_capture_root_allocates_private_distinct_children(tmp_path): @pytest.mark.skipif(os.name != 'posix', reason='Unix local Host transport') @pytest.mark.parametrize('release', ['exact', 'wrong_digest', 'disconnect', 'changed_capture']) -def test_real_cli_handoff_preserves_original_receipt(tmp_path, release): +@pytest.mark.parametrize('native_context', [False, True]) +def test_real_cli_handoff_preserves_original_receipt(tmp_path, release, native_context): import hashlib import socket import sys @@ -322,8 +320,12 @@ def test_real_cli_handoff_preserves_original_receipt(tmp_path, release): from pathlib import Path project, runtime, registry = _write_fixture(tmp_path) - _configure_read_only_todo(project) + state = _configure_read_only_todo(project) + initialize_canonical_source_fixture(runtime, GOAL_ID, state_path=state) capture = tmp_path / 'handoff-capture' + context = tmp_path / 'native-context' + if native_context: + context.mkdir(mode=0o700) with tempfile.TemporaryDirectory(prefix='quota-host-', dir='/tmp') as short: endpoint = Path(short).resolve() / 'host.sock' with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: @@ -337,8 +339,10 @@ def test_real_cli_handoff_preserves_original_receipt(tmp_path, release): '--runtime-profile', 'generic_cli', '--available-capability', 'shell', '--available-capability', 'filesystem_read', '--decision-output-dir', str(capture), '--decision-handoff-socket', str(endpoint)] + if native_context: + command += ['--context-delivery', 'on', '--codex-context-dir', str(context)] with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True, env={**os.environ, 'PYTHONPATH': str(Path(__file__).resolve().parents[2])}) as process: + text=True, env={**os.environ, 'PYTHONPATH': str(Path(__file__).resolve().parents[2]), **({'LOOPX_CONTEXT_DELIVERY': 'on', 'LOOPX_EXECUTION_MODE': 'heartbeat'} if native_context else {})}) as process: try: channel, _ = listener.accept() with channel: @@ -354,6 +358,8 @@ def test_real_cli_handoff_preserves_original_receipt(tmp_path, release): # successful output while the Host is taking its snapshot. import select assert not select.select([process.stdout], [], [], 0)[0] + if native_context: + assert list(context.iterdir()) == [] # no projection before release if release == 'changed_capture': (capture / 'decision.json').write_bytes(original + b'\n') if release != 'disconnect': @@ -366,6 +372,8 @@ def test_real_cli_handoff_preserves_original_receipt(tmp_path, release): else: assert process.returncode != 0 assert 'Do not blindly rerun' in stderr + stdout + if native_context: + assert list(context.iterdir()) == [] assert _heartbeat_receipt_count(runtime, TURN_ID) == receipts assert json.loads(original)['heartbeat_receipt']['turn_instance_id'] == TURN_ID finally: @@ -428,6 +436,8 @@ def test_capture_root_accepts_guided_turn_before_identity_is_minted(tmp_path): def test_guided_turn_captures_the_minted_identity(tmp_path): project, runtime, registry = _write_fixture(tmp_path) + initialize_canonical_source_fixture(runtime, GOAL_ID, + state_path=project / f'.codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md') capture_root = tmp_path / "captures" capture_root.mkdir() @@ -473,6 +483,8 @@ def _replan_selection_hop(tmp_path, host): project, runtime, registry = _write_fixture(tmp_path) _configure_selected_todo_replan_fixture(project, registry) + initialize_canonical_source_fixture(runtime, GOAL_ID, + state_path=project / f'.codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md') common = ('--goal-id', GOAL_ID, '--agent-id', AGENT_ID, '--runtime-profile', 'generic_cli', '--turn-instance-id', TURN_ID, '--available-capability', 'shell', '--available-capability', 'filesystem_write') @@ -524,9 +536,21 @@ def typed_host(observed): code, selected, observed, saved, _ = _replan_selection_hop(tmp_path, typed_host) assert observed['decision']['ok'] is True assert observed['decision']['heartbeat_receipt']['status'] == 'upgraded' - # The binding hop is the same Turn's pending closeout, not a new point. - assert observed['intent'] == {'capture': False, 'reason': 'turn_closeout_pending', - 'execution_authority_granted': False} + # Binding a Todo does not discharge the original semantic replan. This + # fixture has not admitted delivery or produced work/closeout evidence. + # Preserve that boundary instead of inferring closeout from "upgraded". + decision = observed['decision'] + receipt = decision['heartbeat_receipt'] + assert receipt.get('closeout_required') is not True + assert decision['interaction_contract']['agent_channel']['delivery_allowed'] is False + assert decision['replan_action_packet']['decision'] == 'replan_required' + assert decision['replan_action_packet'].get('settlement_only') is not True + assert observed['intent'] == { + 'capture': True, + 'obligation_id': receipt['semantic_replan_obligation_id'], + 'settlement_effect_id': receipt['settlement_identity']['effect_id'], + 'execution_authority_granted': False, + } assert code == 0 and selected['ok'] is True, selected assert selected['schema_version'] == 'loopx_turn_envelope_v0' assert len(saved) == 1 and json.loads(saved[0].read_text()) == observed['decision'] diff --git a/tests/control_plane/test_saved_context_project.py b/tests/control_plane/test_saved_context_project.py new file mode 100644 index 0000000000..c06c60f60f --- /dev/null +++ b/tests/control_plane/test_saved_context_project.py @@ -0,0 +1,309 @@ +"""Saved quota presentation never reevaluates the Goal or renews authority.""" +import hashlib +import json +import os +from pathlib import Path +import shlex +import sys + +import pytest + +from tests.control_plane.test_quota_settlement_cli import _run_cli + + +@pytest.mark.parametrize("failure", [None, "digest", "goal", "agent", "turn", "symlink", "object"]) +def test_saved_projection_checks_original_before_any_publication(tmp_path, failure): + original = {"ok": True, "goal_id": "saved-goal", "agent_identity": {"agent_id": "saved-agent"}, + "heartbeat_receipt": {"turn_instance_id": "saved-turn"}, "requirement": "保留完整要求🙂" * 100} + path = tmp_path / "original.json" + path.write_text(json.dumps(original, ensure_ascii=False)) + if failure == "object": + path.write_text("[]") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + if failure == "symlink": + source = tmp_path / "source.json" + path.rename(source) + path.symlink_to(source) + args = ["quota", "context-project", "--capture-file", str(path), "--capture-sha256", digest, + "--goal-id", "saved-goal", "--agent-id", "saved-agent", "--turn-instance-id", "saved-turn"] + for name in ("digest", "goal", "agent", "turn"): + if failure == name: + option = {"digest": "--capture-sha256", "goal": "--goal-id", "agent": "--agent-id", "turn": "--turn-instance-id"}[name] + args[args.index(option) + 1] = "0" * 64 if name == "digest" else "foreign" + registry, runtime = tmp_path / "absent-registry.json", tmp_path / "absent-runtime" + code, result = _run_cli(registry, runtime, *args, cwd=tmp_path) + assert not registry.exists() and not runtime.exists() + if failure: + assert code != 0 and not result["ok"] and "command" not in result + else: + assert code == 0 and result["ok"] and result["mode"] == "full" + assert result["path"] == str(path) and result["sha256"] == digest + assert result["recovery"]["path"] == str(path) + command = shlex.split(result["command"]) + code, page = _run_cli(registry, runtime, *command[3:], cwd=tmp_path) + assert code == 0 and page["content"].encode() == path.read_bytes()[:page["next_offset"]] + + +@pytest.mark.parametrize("root_kind", ["unsafe", "symlink", "missing"]) +def test_optional_cache_failure_returns_original_full_reference(tmp_path, root_kind): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import project_saved_context_capture + + path, root, context = tmp_path / "original.json", tmp_path / "cache", tmp_path / "native" + raw = json.dumps({"ok": True, "goal_id": "saved-goal", "agent_identity": {"agent_id": "saved-agent"}, + "heartbeat_receipt": {"turn_instance_id": "saved-turn"}}).encode() + path.write_bytes(raw) + context.mkdir(mode=0o700) + if root_kind == "unsafe": + root.mkdir(mode=0o755) + root.chmod(0o755) + elif root_kind == "symlink": + root.symlink_to(context, target_is_directory=True) + 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", + "--agent-id", "saved-agent", "--turn-instance-id", "saved-turn", + "--decision-output-root", str(root), "--context-delivery", "on", "--codex-app", "--codex-context-dir", str(context)]) + result = project_saved_context_capture(args) + assert result["ok"] and result["mode"] == "full" + assert result["path"] == str(path) and result["sha256"] == digest + assert path.read_bytes() == raw and list(context.iterdir()) == [] + + +def test_preflight_instruction_pins_both_projection_and_full_fallback(tmp_path): + from benchmark.runtime.context_delivery import preflight_context_instruction + + source = tmp_path / "saved packet.json" + env = {"LOOPX_CLI": "/fixture/bin/loopx", "LOOPX_GOAL_ID": "goal", + "LOOPX_AGENT_ID": "agent", "LOOPX_CODEX_CONTEXT_DIR": str(tmp_path / "private native")} + body = preflight_context_instruction(env, source, "a" * 64, "original-turn") + commands = [shlex.split(block.split("\n```", 1)[0]) for block in body.split("```sh\n")[1:]] + assert len(commands) == 2 + assert commands[0][6] == "context-project" and commands[1][6] == "context-read" + for argv in commands: + assert argv[:3] == [env["LOOPX_CLI"], "--cli-bin", env["LOOPX_CLI"]] + assert argv[argv.index("--capture-file") + 1] == str(source) + assert argv[argv.index("--capture-sha256") + 1] == "a" * 64 + assert "should-run" not in argv and "spend-slot" not in argv + assert commands[0][commands[0].index("--turn-instance-id") + 1] == "original-turn" + + +@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"]) +def test_native_saved_preflight_uses_current_input_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 + from tests.control_plane.codex_checkpoint_fixture import local_inference_provider + from tests.control_plane.test_codex_context_boundary import _app_server, _configure_provider, _native_fixture, _input_text + from tests.control_plane.test_codex_context_capture import _delivered + from tests.control_plane.test_quota_settlement_cli import AGENT_ID, GOAL_ID, TODO_ID, _write_fixture, _spend_run_count, _heartbeat_receipt_count + + 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")) + project, runtime, registry = _write_fixture(workspace) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + todos = read_fixture_todos(state) + todos[0]["text"] = todos[0]["title"] = "Exact requirement with retained provenance.\n" * 240 + 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="sqlite") + sources = [] + for name in (("cold", "warm") if host == "app_server" else ()): + code, decision = _run_cli(registry, runtime, "quota", "should-run", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", "saved-native-turn", "--codex-app", cwd=project) + assert code == 0, decision + source = workspace / (name + "-source.json") + source.write_text(json.dumps(decision, ensure_ascii=False)) + sources.append((source, source.read_bytes())) + receipts = _heartbeat_receipt_count(runtime, "saved-native-turn") + context, cache = workspace / "context", workspace / "cache" + context.mkdir(mode=0o700) + cache.mkdir(mode=0o700) + program = os.environ.get("LOOPX_NATIVE_CONTEXT_CLI") or str(Path(sys.executable).with_name("loopx")) + cli = [program, "--cli-bin", program] + from loopx.control_plane.turn_driver.codex_context import codex_context_configuration + configuration = codex_context_configuration(str(context), goal_id=GOAL_ID, agent_id=AGENT_ID, cli_argv=cli, scenario="codex_app_heartbeat") + if host == "app_server": + (home / "hooks.json").write_text(json.dumps({"hooks": configuration["hooks"]})) + phase, queue, observed, errors = "cold", [], {}, [] + actual_turn = None + + def response(request, number, message): + nonlocal phase, queue, actual_turn, receipts + try: + if queue: + command = queue.pop(0) + elif phase in ("cold", "warm"): + if host == "benchmark": + command = [line for line in _input_text(request).splitlines() if " quota context-project " in line][-1] + argv = shlex.split(command) + actual_turn = argv[argv.index("--turn-instance-id") + 1] + source = Path(argv[argv.index("--capture-file") + 1]) + sources.append((source, source.read_bytes())) + receipts = _heartbeat_receipt_count(runtime, actual_turn) + assert hashlib.sha256(source.read_bytes()).hexdigest() == argv[argv.index("--capture-sha256") + 1] + else: + source, raw = sources[0 if phase == "cold" else 1] + command = shlex.join([*cli, "--format", "json", "quota", "context-project", + "--capture-file", str(source), "--capture-sha256", hashlib.sha256(raw).hexdigest(), + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "saved-native-turn", + "--context-delivery", "on", "--codex-app", "--codex-context-dir", str(context), "--decision-output-root", str(cache)]) + phase += "_ready" + elif phase.endswith("_ready"): + name = phase.split("_")[0] + output = [item["output"] for item in request["input"] if item.get("type") == "function_call_output"][-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()) + assert descriptor["target"] == wire["target"] + observed[name] = (wire, descriptor) + data = Path(descriptor["path"]).read_bytes() + for offset in range(0, len(data), 4096): + # Synthetic captures are ASCII JSON; public reader owns every byte. + queue.append(shlex.join([*cli, "--format", "json", "quota", "context-read", + "--capture-file", descriptor["path"], "--capture-sha256", descriptor["sha256"], + "--offset", str(offset)])) + phase = name + "_read" + command = queue.pop(0) + else: + name = phase.split("_")[0] + descriptor = observed[name][1] + assert _delivered(request, descriptor["sha256"]) == Path(descriptor["path"]).read_text().rstrip("\n") + if name == "warm": + phase = "done" + return message + # Full wire context is observed exactly, then reuse in the next call. + phase = "warm" + if host == "benchmark": + return message # A new worker wake owns its own original guard. + return response(request, number, message) + return {"type": "function_call", "id": f"fc_{number}", "call_id": f"call_{number}", + "name": "exec_command", "arguments": json.dumps({"cmd": command, "max_output_tokens": 3000})} + except Exception as error: + errors.append(error) + return message + + with local_inference_provider(response) as (url, _, _): + if host == "benchmark": + from benchmark.runtime.worker import run_once + skills = workspace / "skills" + skills.mkdir() + declaration = workspace / "checkpoint-request.json" + declaration.write_text(json.dumps({"trigger": "fresh_replan", "destination": str(workspace / "checkpoints"), + "components": [{"id": "task", "source_path": str(state)}], "point_metadata": {}})) + env.update(LOOPX_CONTEXT_DELIVERY="on", LOOPX_EXECUTION_MODE="heartbeat", LOOPX_ITERATION_CONTEXT="resume", + LOOPX_TASK_ENTRY="seeded-todo", LOOPX_TASK_STAGE="execute", LOOPX_CHECKPOINT_REQUEST=str(declaration), + LOOPX_CODEX_HOME=str(home), LOOPX_PROJECT=str(project), LOOPX_SHARED_SKILLS=str(skills), + LOOPX_REGISTRY=str(registry), LOOPX_RUNTIME_ROOT=str(runtime), LOOPX_GOAL_ID=GOAL_ID, LOOPX_AGENT_ID=AGENT_ID, + LOOPX_WAKE_LOG_DIR=str(workspace / "wakes"), LOOPX_CLI=program, CODEX_BIN=binary, + MODEL_NAME="fixture", REASONING_EFFORT="high", OPENAI_BASE_URL=url, OPENAI_API_KEY="synthetic-loopback-only", + LOOPX_CODEX_TURN_TIMEOUT_SEC="60") + result = run_once(env) + assert result["ok"] and result["context_transport"] == {"available": True}, result + session_id = result["session"]["session_id"] + assert phase == "warm" and sources[0][0].read_bytes() == sources[0][1] + # Settle this isolated, independently verified readback once before + # a new wake. Presentation itself still performs no debit or guard. + cold_turn = json.loads(sources[0][1])["heartbeat_receipt"]["turn_instance_id"] + binding = ("--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", cold_turn) + code, written = _run_cli(registry, runtime, "refresh-state", *binding, + "--classification", "saved_context_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 code == 0, written + code, spent = _run_cli(registry, runtime, "quota", "spend-slot", *binding, + "--slots", "1", "--source", "heartbeat", "--execute", cwd=project) + assert code == 0 and spent["appended"], spent + result = run_once(env) + assert result["ok"] and result["context_transport"] == {"available": True}, result + assert result["session"]["session_id"] == session_id + else: + _configure_provider(home, url) + with (home / "config.toml").open("a") as config: + config.write("[sandbox_workspace_write]\nnetwork_access=true\n") + _app_server(binary, env, workspace, home, True, sandbox="workspace-write") + assert not errors and phase == "done", errors + if host == "benchmark": + turns = [json.loads(raw)["heartbeat_receipt"]["turn_instance_id"] for _, raw in sources] + assert len(set(turns)) == 2 + assert all(_heartbeat_receipt_count(runtime, turn) == 1 for turn in turns) + assert _spend_run_count(runtime) == 1 # only the explicit inter-wake settlement above + else: + assert _heartbeat_receipt_count(runtime, "saved-native-turn") == receipts and _spend_run_count(runtime) == 0 + cold, warm = observed["cold"][0], observed["warm"][0] + assert cold["context"] == json.loads(sources[0][1]) + original = json.loads(sources[1][1]) + current = prepare_context_delivery(original, + scope={key: warm["target"][key] for key in ("goal_id", "agent_id", "session_id", "context_epoch")}, + capture_id=warm["target"]["capture_id"], surface="quota_packet") + assert restore_context_delivery(warm, baseline=cold, current=current)["context"] == original + assert all(path.read_bytes() == raw for path, raw in sources) + + +@pytest.mark.skipif(not os.environ.get("LOOPX_NATIVE_CHECKPOINT_BIN"), reason="Explicit native Codex; loopback inference only") +def test_native_discovery_keeps_plugins_out_of_probe_without_changing_solver(tmp_path, monkeypatch): + from contextlib import contextmanager + from benchmark.runtime.context_delivery import prepare_context_delivery + from loopx.control_plane.turn_driver.codex_stdio import StdioNativeGoalTransport + from tests.control_plane.codex_checkpoint_fixture import local_inference_provider + from tests.control_plane.test_codex_context_boundary import _configure_provider, _native_fixture + + binary, env, workspace, home = _native_fixture(tmp_path) + observed = [] + original_spawn = StdioNativeGoalTransport.spawn + + @contextmanager + def inspect_probe(argv, **kwargs): + with original_spawn(argv, **kwargs) as native: + def request(method, params): + result = native.request(method, params) + if method == "hooks/list": + effective = native.request("config/read", {"cwd": str(workspace), "includeLayers": False}) + observed.append(effective["config"]["features"]) + return result + from types import SimpleNamespace + yield SimpleNamespace(request=request, notify=native.notify) + + monkeypatch.setattr(StdioNativeGoalTransport, "spawn", inspect_probe) + with local_inference_provider() as (url, calls, _): + _configure_provider(home, url) + config = home / "config.toml" + config.write_text(config.read_text().replace("plugins=false", "plugins=true") + .replace("remote_plugin=false", "remote_plugin=true") + .replace("plugin_sharing=false", "plugin_sharing=true")) + original_config = config.read_bytes() + env.update(CODEX_BIN=binary, LOOPX_PROJECT=str(workspace), LOOPX_GOAL_ID="fixture", + LOOPX_AGENT_ID="agent", LOOPX_CLI=str(Path(sys.executable).with_name("loopx"))) + prepare_context_delivery(env, home) + first_trust = env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"] + prepare_context_delivery(env, home) + assert env["LOOPX_CODEX_CONTEXT_TRUST_ARGS"] == first_trust + assert calls == [] # Hook identity lookup never starts inference. + assert len(observed) == 2 + for features in observed: + assert all(features[key] is False for key in ("plugins", "remote_plugin", "plugin_sharing")) + assert config.read_bytes() == original_config + assert not list(home.rglob("*.pack")), "discovery must not clone a plugin catalog" + + +def test_saved_projection_default_off_ignores_available_native_cache(tmp_path, monkeypatch): + from loopx.cli import build_parser + from loopx.cli_commands.quota_capture import project_saved_context_capture + from loopx.control_plane.turn_driver import codex_context + + path, root = tmp_path / "original.json", tmp_path / "uncreated" + raw = json.dumps({"ok": True, "goal_id": "g", "agent_identity": {"agent_id": "a"}, + "heartbeat_receipt": {"turn_instance_id": "t"}}).encode() + path.write_bytes(raw) + def unexpected(*args, **kwargs): + pytest.fail("default-off saved presentation must not inspect/create native state") + monkeypatch.setattr(codex_context, "codex_context_directory", unexpected) + args = build_parser().parse_args(["quota", "context-project", "--capture-file", str(path), + "--capture-sha256", hashlib.sha256(raw).hexdigest(), "--goal-id", "g", "--agent-id", "a", + "--turn-instance-id", "t", "--codex-context-dir", str(root), "--decision-output-root", str(root)]) + result = project_saved_context_capture(args) + assert result["ok"] and result["mode"] == "full" and result["path"] == str(path) + assert not root.exists() and path.read_bytes() == raw diff --git a/tests/control_plane_ts/checkpoint_artifacts.test.ts b/tests/control_plane_ts/checkpoint_artifacts.test.ts index e35aab5512..1c466387e4 100644 --- a/tests/control_plane_ts/checkpoint_artifacts.test.ts +++ b/tests/control_plane_ts/checkpoint_artifacts.test.ts @@ -349,9 +349,9 @@ test("receipt-owned replan settlement returns to its original writer without adm ...quota.replan_action_packet, decision: "replan_required", settlement_only: false}}})).capture, true); }); -test("the selection hop that binds a Todo is released as pending closeout, not an identity failure", async () => { - // Real-CLI shape: the agent selects a Todo on the Turn whose original guard - // owns the replan. The receipt view is "upgraded" with the new binding. +test("a bound selection with explicit closeout is released; binding alone preserves the replan", async () => { + // "upgraded" records the binding, not work completion. Only the explicit + // closeout fact makes this a pending closeout instead of the original replan. const id = "replan-0123456789abcdef", turn = "fixture-turn"; const identity = {goal_id: "fixture-goal", agent_id: "fixture-agent", turn_instance_id: turn, todo_id: "todo_selected", effect_id: `fixture-goal:fixture-agent:todo_selected:${turn}`}; @@ -363,6 +363,12 @@ test("the selection hop that binds a Todo is released as pending closeout, not a assert.deepEqual(await checkpointArtifacts(request), { capture: false, reason: "turn_closeout_pending", execution_authority_granted: false, }); + const {closeout_required: _closeout, ...boundReceipt} = quota.heartbeat_receipt; + assert.deepEqual(await checkpointArtifacts({...request, quota: {...quota, + heartbeat_receipt: boundReceipt}}), { + capture: true, obligation_id: id, settlement_effect_id: identity.effect_id, + execution_authority_granted: false, + }); // Statuses outside the durable receipt views stay refused, now as a typed // request rejection whose reason survives the runtime boundary. for (const status of ["pending", "failed", "uncommitted"]) { diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts new file mode 100644 index 0000000000..d6394e157e --- /dev/null +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -0,0 +1,360 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; +import {compactSelectedTodoSource, prepareContextDelivery, restoreContextDelivery} from "../../loopx/control_plane/quota/context_delivery.ts"; + +import {evaluateTurnEnvelope} from "../../loopx/control_plane/quota/turn_envelope.ts"; + +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}}, + current_control: {labels: ["original"]}}; + const current = {...structuredClone(original), current_control: {labels: ["current"]}}; + current.registry.changed.value = 2; + const before = structuredClone(original), after = structuredClone(current); + const initial = prepareContextDelivery({context: original, scope, capture_id: "original", surface: "quota_packet"}); + const args = {context: current, scope, capture_id: "current", surface: "quota_packet"}; + const delta = prepareContextDelivery({...args, baseline: initial, retained: {complete: true, target: initial.target}}); + assert.equal(delta.mode, "delta"); + assert.deepEqual(inline(delta, ["current_control"]), {labels: ["current"]}); + const restored = restoreContextDelivery({delivery: delta, baseline: initial, current: prepareContextDelivery(args)}); + assert.deepEqual(restored.context, after); + // Publication must detach both a forced whole value and a sparse replacement. + (inline(delta, ["current_control"]) as {labels: string[]}).labels.push("wire-only"); + (restored.context as typeof current).registry.changed.value = 3; + assert.deepEqual(original, before); + assert.deepEqual(initial.context, before); + assert.deepEqual(current, after); +}); + +const scope = {goal_id: "goal", agent_id: "agent", session_id: "session", context_epoch: "epoch-1"}; +const long = "Keep the original acceptance and stop before publication. ".repeat(400); +function context(): JsonObject { + return {complete: true, instruction: "Read requirements before work.", sources: [ + {source: "goal_state", command: "read-goal", content: {state_file: "goal.md", revision: "revision-a", + excluded_todo_regions: [{role: "agent", start_line: 4, end_line: 6, revision: "tasks-a"}], text: long}}, + {source: "selected_todo", command: "read-task", content: {ok: true, matched: true, + authority_read: {provider_revision: "a"}, todo: {todo_id: "todo_work", text: long, + note: "Keep the validation evidence.", status: "open", claimed_by: "agent"}}}, + {source: "goal_acceptance", content: {provider_revision: "a", goal_acceptance_contract: { + outcome_requirements: {revision: 1, outcomes: [{id: "outcome-a", objective: long, + requirements: [{id: "r1", description: long, status: "unproven", agent_review: {note: "No evidence yet."}}]}]}}}}, + {kind: "unknown_source", content: {large_mixed_control: long}}, + ]}; +} +const source = (value: JsonObject, index: number): JsonObject => (value.sources as JsonObject[])[index].content as JsonObject; +const full = (value: JsonObject, capture_id = "capture-1", extraScope = scope): JsonObject => + prepareContextDelivery({context: value, scope: extraScope, capture_id}); +function warm(value: JsonObject, baseline: JsonObject): JsonObject { + return prepareContextDelivery({context: value, scope, capture_id: "capture-2", baseline, + retained: {complete: true, target: baseline.target}}); +} +const recover = (delivery: JsonObject, baseline: JsonObject, value: JsonObject): JsonObject => + restoreContextDelivery({delivery, baseline, current: full(value, "capture-2")}); + +// Read an inline value from the wire, without borrowing any baseline content. +// Tests use this independent walker to verify controls really reach each input. +function inline(delivery: JsonObject, path: (string | number)[]): unknown { + let node = delivery.changes as JsonObject; + for (let i = 0; ; i++) { + if (!node) return undefined; + if (Object.hasOwn(node, "value")) { + let value = node.value; + for (const key of path.slice(i)) value = (value as JsonObject)?.[key]; + return value; + } + if (i === path.length) return undefined; + node = ((node.changed ?? node.items) as Record)?.[path[i]]; + } +} + +test("full first; unsupported or uncertain retention never becomes an Agent gate", () => { + const original = context(), saved = structuredClone(original), baseline = full(original); + assert.equal(baseline.mode, "full"); + assert.deepEqual(baseline.context, original); + for (const retained of [undefined, {complete: false, target: baseline.target}, {complete: true, target: {}}]) { + const result = prepareContextDelivery({context: original, scope, capture_id: "capture-2", baseline, retained}); + assert.equal(result.mode, "full"); + assert.deepEqual(result.context, original); + } + assert.deepEqual(original, saved); +}); + +test("fresh control with exact warm bodies; changed source revisions do not resend requirements", () => { + const original = context(), baseline = full(original), current = structuredClone(original); + source(current, 0).revision = "revision-b"; + source(current, 1).authority_read = {provider_revision: "b"}; + (source(current, 1).todo as JsonObject).claimed_by = "different-agent"; + const requirements = (((source(current, 2).goal_acceptance_contract as JsonObject).outcome_requirements as JsonObject).outcomes as JsonObject[])[0].requirements as JsonObject[]; + requirements[0].status = "failed"; + requirements[0].agent_review = {note: "New failure invalidates the earlier plan."}; + const result = warm(current, baseline); + assert.equal(result.mode, "delta"); + assert.equal(inline(result, ["sources", 0, "content", "revision"]), "revision-b"); + assert.equal(inline(result, ["sources", 1, "content", "todo", "claimed_by"]), "different-agent"); + assert.equal(inline(result, ["sources", 3, "content", "large_mixed_control"]), long); + assert.deepEqual(recover(result, baseline, current).context, current); + assert.ok(JSON.stringify(result).length < JSON.stringify(full(current)).length / 2); + assert.deepEqual(baseline.context, original); +}); + +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[]; + (outcomes[0].requirements as JsonObject[]).push({id: "r2", description: "Keep a distinct requirement", status: "unproven"}); + const baseline = full(initial), current = structuredClone(initial); + const currentOutcomes = ((source(current, 2).goal_acceptance_contract as JsonObject).outcome_requirements as JsonObject).outcomes as JsonObject[]; + (currentOutcomes[0].requirements as JsonObject[]).reverse(); + (current.sources as JsonObject[]).reverse(); + const delta = warm(current, baseline); + if (delta.mode === "full") { + assert.equal(delta.reason, "delta_not_smaller"); + assert.deepEqual(delta.context, current); + } else assert.deepEqual(recover(delta, baseline, current).context, current); +}); + +test("late constraints, null, empty and removed fields remain distinct", () => { + for (const value of ["Late constraint: never delete the original source.", "", null, undefined]) { + const initial = context(), baseline = full(initial), current = structuredClone(initial); + const task = source(current, 1).todo as JsonObject; + if (value === undefined) delete task.note; + else task.note = value; + const delta = warm(current, baseline); + assert.equal(delta.mode, "delta"); + + const restored = recover(delta, baseline, current); + assert.equal(restored.mode, "delta"); + assert.deepEqual(restored.context, current); + } +}); + +test("unexpected structured values in text-only adapters stay full", () => { + const initial = context(), baseline = full(initial); + for (const note of [{stop: true}, ["A", "B"], 42]) { + const current = structuredClone(initial); + (source(current, 1).todo as JsonObject).note = note; + const result = warm(current, baseline); + assert.equal(result.mode, "full"); + assert.deepEqual(result.context, current); + } +}); + +test("scope/epoch/version, corruption, loss and ancestry automatically select full", () => { + const initial = context(), baseline = full(initial); + for (const key of ["goal_id", "agent_id", "session_id", "context_epoch", "projection_version"]) { + const wrong = structuredClone(baseline); (wrong.target as JsonObject)[key] = "other"; + assert.equal(warm(initial, wrong).mode, "full", key); + } + const corrupt = structuredClone(baseline); (corrupt.context as JsonObject).instruction = "Changed after ACK"; + assert.equal(warm(initial, corrupt).mode, "full"); + assert.equal(warm(initial, {...baseline, ancestry: 32}).mode, "full"); + const tiny = {complete: true, sources: []}; + assert.equal(warm(tiny, full(tiny)).mode, "full"); + const failed = {...initial, complete: false}; + assert.equal(warm(failed, baseline).mode, "full"); + const duplicate = structuredClone(initial); + (duplicate.sources as JsonObject[]).push(structuredClone((initial.sources as JsonObject[])[0])); + assert.equal(warm(duplicate, baseline).mode, "full"); +}); + +test("final assembly race, truncated or out-of-order delta uses same-capture full", () => { + const initial = context(), baseline = full(initial), current = structuredClone(initial); + source(current, 0).text = long + "NEW STOP CONDITION"; + const delta = warm(current, baseline), capture = full(current, "capture-2"); + const corruptions = [ + {...delta, changes: null}, {...delta, changes: {removed: ["sources"]}}, {...delta, ancestry: 20}, + {...delta, changes: {items: {"-1": {value: "malformed"}}, length: -1}}, + {...delta, base: {...baseline.target as JsonObject, capture_id: "lost"}}, + {...delta, changes: {value: {complete: true}}}, + ]; + for (const delivery of corruptions) { + const restored = restoreContextDelivery({delivery, baseline, current: capture}); + assert.equal(restored.mode, "full"); + assert.deepEqual(restored.context, current); + } + // Epoch moves after preparation but before the final model request. + const resetCapture = full(current, "capture-2", {...scope, context_epoch: "epoch-2"}); + const reset = restoreContextDelivery({delivery: delta, baseline, current: resetCapture}); + assert.equal(reset.mode, "full"); + assert.deepEqual(reset.context, current); + // Missing acknowledgement leaves the previous baseline in place: a later + // delta referring to another capture cannot advance it retroactively. + const retained = {...delta, context: current}; + const next = warm(current, retained); + assert.equal(recover(next, baseline, current).mode, "full"); +}); + +test("cold-source title deduplication retains full body, distinct title and original capture", () => { + const original = {todo: {text: long, title: long, note: "Keep this note", status: "open"}}; + const result = compactSelectedTodoSource(original); + assert.deepEqual(result, {todo: {text: long, note: "Keep this note", status: "open"}}); + assert.equal(original.todo.title, long); + const distinct = {todo: {...original.todo, title: "A meaningful short title"}}; + assert.strictEqual(compactSelectedTodoSource(distinct), distinct); +}); + +function evidenceContext(): JsonObject { + return {complete: true, sources: [{source: "turn_start_capability_hook", kind: "explore_turn_context", + hook_id: "explore.turn_context", capability_id: "explore", content: { + ok: true, goal_id: "goal", agent_id: "agent", graph_enabled: true, harness_enabled: true, + graph: {writeback_results: [{finding_id: "f1", status: "confirmed", summary: long, + evidence_refs: ["artifact:source"], execution_observation: {scope: "candidate-a"}}], + measurement_feedback: [{metric: "cost", value: 3, unit: "tokens", input_revision: "input-a"}], + result_page: {revision: "r1", remaining: 0}, guidance: long, + unknown_future_gate: {allowed: true}}, + harness: {orchestration_gate: {allowed: true}, selected_branches: [{todo_id: "task-a", status: "open"}], + guidance: long}, boundary: {starts_agents: false}}}]}; +} + +test("registered structured evidence is reusable; live gates and unknown fields stay fresh", () => { + const initial = evidenceContext(), baseline = full(initial), current = structuredClone(initial); + const graph = source(current, 0).graph as JsonObject, harness = source(current, 0).harness as JsonObject; + graph.result_page = {revision: "r2", remaining: 1, next_command: "read-next-page"}; + graph.unknown_future_gate = {allowed: false}; + harness.orchestration_gate = {allowed: false, reason: "stop"}; + harness.selected_branches = []; + const delta = warm(current, baseline); + assert.equal(delta.mode, "delta"); + assert.equal(inline(delta, ["sources", 0, "content", "graph", "writeback_results"]), undefined); + assert.equal(inline(delta, ["sources", 0, "content", "graph", "result_page", "revision"]), "r2"); + assert.equal(inline(delta, ["sources", 0, "content", "graph", "unknown_future_gate", "allowed"]), false); + assert.equal(inline(delta, ["sources", 0, "content", "harness", "orchestration_gate", "allowed"]), false); + assert.deepEqual(inline(delta, ["sources", 0, "content", "harness", "selected_branches"]), []); + assert.deepEqual(recover(delta, baseline, current).context, current); +}); + +test("changed evidence retains exact provenance, null and late refutation", () => { + const initial = evidenceContext(), baseline = full(initial); + for (const evidence of [[], null, [{finding_id: "f1", status: "refuted", summary: "A new counterexample.", + evidence_refs: ["artifact:new-source"], execution_observation: {scope: "candidate-b"}}], undefined]) { + const current = structuredClone(initial), graph = source(current, 0).graph as JsonObject; + if (evidence === undefined) delete graph.writeback_results; + else graph.writeback_results = evidence; + const delta = warm(current, baseline); + assert.equal(delta.mode, "delta"); + + assert.deepEqual(recover(delta, baseline, current).context, current); + } +}); + +test("unknown/failed hook and ambiguous scope never borrow a registered hook's evidence", () => { + const initial = evidenceContext(), baseline = full(initial); + for (const field of ["hook_id", "capability_id", "kind"]) { + const current = structuredClone(initial); + (current.sources as JsonObject[])[0][field] = "unknown"; + assert.equal(warm(current, baseline).mode, "full"); + } + const failed = structuredClone(initial); source(failed, 0).ok = false; + assert.equal(warm(failed, baseline).mode, "full"); + const duplicate = structuredClone(initial); + (duplicate.sources as JsonObject[]).push(structuredClone((initial.sources as JsonObject[])[0])); + assert.equal(warm(duplicate, baseline).mode, "full"); + const different = structuredClone(initial); source(different, 0).todo_id = "different-task"; + const delta = warm(different, baseline); + assert.equal(inline(delta, ["sources", 0, "content", "todo_id"]), "different-task"); + assert.deepEqual(recover(delta, baseline, different).context, different); +}); + +function packet(): JsonObject { + return {goal_id: "goal", agent_id: "agent", should_run: true, selected_todo: {todo_id: "todo_work", claimed_by: "agent"}, + goal_boundary: {write_scope: ["src/**"], approval: "pending", stop: "publication"}, + unknown_future_control: {allowed: false}, handoff_readiness: {ready: false}, + automation_liveness: {pause_allowed: false}, autonomous_replan_scope: {owner_agent_ids: ["agent"]}, planning_horizon: {items: [{id: "one", body: long}, {id: "two", body: long}]}, + interaction_contract: {mode: "bounded_delivery", user_channel: {notify: "quiet"}, + agent_channel: {delivery_allowed: true, must_attempt: true, work_context: context()}, + cli_channel: {spend_policy: "after_validation", settlement_plan: {identity: {turn: "turn-a"}, ordered_steps: [ + {kind: "validation", owner: "agent", precondition: long, command_template: "validate-a", command_condition: "todo_complete"}, + {kind: "writeback", owner: "agent", precondition: long, command_template: "writeback-a", optional_attachments: [{capability_id: "explore", guidance: long, required: false}]}, + ]}}}}; +} +const packetFull = (value: JsonObject, capture_id = "capture-a", surface = "quota_packet") => + prepareContextDelivery({context: value, scope, capture_id, surface}); +function packetWarm(value: JsonObject, baseline: JsonObject, surface = "quota_packet") { + return prepareContextDelivery({context: value, scope, capture_id: "capture-b", surface, baseline, + retained: {complete: true, target: baseline.target}}); +} + +test("whole packet reuses complete projections; live controls and unknown fields remain inline", () => { + const before = packet(), baseline = packetFull(before), current = structuredClone(before); + const cli = ((current.interaction_contract as JsonObject).cli_channel as JsonObject); + const plan = cli.settlement_plan as JsonObject; + plan.identity = {turn: "turn-b"}; + (plan.ordered_steps as JsonObject[])[1].command_template = "writeback-b"; + const delta = packetWarm(current, baseline); + assert.equal(delta.mode, "delta"); + for (const [path, value] of [ + [["interaction_contract", "cli_channel", "settlement_plan", "ordered_steps", 1, "optional_attachments", 0, "required"], false], + [["handoff_readiness", "ready"], false], [["automation_liveness", "pause_allowed"], false], + [["autonomous_replan_scope", "owner_agent_ids"], ["agent"]], [["should_run"], true], [["unknown_future_control", "allowed"], false], + [["selected_todo", "claimed_by"], "agent"], [["goal_boundary", "approval"], "pending"], + [["interaction_contract", "agent_channel", "delivery_allowed"], true], + [["interaction_contract", "cli_channel", "settlement_plan", "identity", "turn"], "turn-b"], + [["interaction_contract", "cli_channel", "settlement_plan", "ordered_steps", 0, "command_condition"], "todo_complete"], + [["interaction_contract", "cli_channel", "settlement_plan", "ordered_steps", 1, "command_template"], "writeback-b"], + ] as [(string | number)[], unknown][]) assert.deepEqual(inline(delta, path), value, JSON.stringify(path)); + const recovered = restoreContextDelivery({delivery: delta, baseline, current: packetFull(current, "capture-b")}); + assert.equal(recovered.mode, "delta"); assert.deepEqual(recovered.context, current); + assert.ok(JSON.stringify(delta).length < JSON.stringify(current).length / 3); + assert.equal(inline(delta, ["planning_horizon"]), undefined); + // A forged wire can reconstruct correctly yet omit an unchanged live gate. + const broken = structuredClone(delta); + delete ((broken.changes as JsonObject).changed as JsonObject).should_run; + assert.equal(restoreContextDelivery({delivery: broken, baseline, current: packetFull(current, "capture-b")}).mode, "full"); +}); + +test("array edits preserve append, removal, reorder, duplicate entries and exact scalar types", () => { + const before = packet(), baseline = packetFull(before); + const old = (before.planning_horizon as JsonObject).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], late_constraint: "STOP now", revision: "r2"}, old[1]]]; + for (const items of cases) { + const current = {...before, planning_horizon: {items}}; + const delta = packetWarm(current, baseline); + const capture = packetFull(current, "capture-b"); + const restored = delta.mode === "full" ? delta : restoreContextDelivery({delivery: delta, baseline, current: capture}); + assert.deepEqual(restored.context, current); + } + assert.deepEqual(before, packet()); +}); + +test("JSON property names are data; prototype keys never become assignment paths", () => { + const before = packet(), baseline = packetFull(before), current = structuredClone(before); + current.planning_horizon = JSON.parse('{"__proto__":{"unsafe":true},"constructor":null,"removed":false}'); + const delta = packetWarm(current, baseline); + const restored = restoreContextDelivery({delivery: delta, baseline, current: packetFull(current, "capture-b")}); + assert.deepEqual(restored.context, current); + assert.equal(({} as JsonObject).unsafe, undefined); +}); + +test("quota and Turn use one codec but cannot borrow another surface's baseline", () => { + const value = packet(), quota = packetFull(value), turn = {schema_version: "loopx_turn_envelope_v0", + action: {kind: "run", allowed: true}, work_context: context(), writeback: {settlement_plan: + ((value.interaction_contract as JsonObject).cli_channel as JsonObject).settlement_plan}}; + const baseline = packetFull(turn, "capture-a", "turn_envelope"); + assert.equal(packetWarm(turn, quota, "turn_envelope").mode, "full"); + const delta = packetWarm(turn, baseline, "turn_envelope"); + assert.equal(delta.mode, "delta"); + assert.equal(inline(delta, ["action", "allowed"]), true); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline, + current: packetFull(turn, "capture-b", "turn_envelope")}).context, turn); +}); + + +test("legacy quota fields are bound by the existing adapter, never caller data", () => { + const value = {...packet(), heartbeat_recommendation: {reason: long}}; + const baseline = evaluateTurnEnvelope({operation: "context_delivery", context: value, + scope, capture_id: "capture-a", surface: "quota_packet"}); + const request = {context: value, scope, capture_id: "capture-b", surface: "quota_packet", baseline, + retained: {complete: true, target: baseline.target}, legacyQuotaFields: ["unknown_future_control"]}; + const delta = evaluateTurnEnvelope({...request, operation: "context_delivery"}); + assert.equal(delta.mode, "delta"); + assert.equal(inline(delta, ["heartbeat_recommendation"]), undefined); + assert.deepEqual(inline(delta, ["unknown_future_control"]), value.unknown_future_control); + const current = evaluateTurnEnvelope({operation: "context_delivery", context: value, + scope, capture_id: "capture-b", surface: "quota_packet"}); + const restored = evaluateTurnEnvelope({operation: "context_recovery", current, delivery: delta, baseline}); + assert.equal(restored.mode, "delta"); + assert.deepEqual(restored.context, value); + // A direct codec call has no legacy binding and conservatively refreshes it. + assert.deepEqual(inline(prepareContextDelivery(request), ["heartbeat_recommendation"]), value.heartbeat_recommendation); +});