From d337d1cab282ace01680e828c497f8c6ae97b625 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:19:57 +0800 Subject: [PATCH] Honor cross-turn delta independently of progressive context Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../machine-configuration-settings.tsx | 6 ++- docs/reference/required-work-context.md | 24 ++++++----- .../configuration-backup.mjs | 12 +++++- .../machine_configuration/context_delivery.py | 20 ++++++--- loopx/cli_commands/context_machine.py | 13 ++++-- loopx/cli_commands/quota_capture.py | 41 +++++++++++------- loopx/cli_commands/quota_registration.py | 2 +- .../cli_commands/support_control_heartbeat.py | 2 +- .../support_control_heartbeat_registration.py | 4 +- loopx/control_plane/heartbeat/builder.py | 8 ++-- .../turn_driver/codex_context.py | 31 ++++++++------ .../test_context_machine_configuration.py | 42 ++++++++++++++++--- .../test_progressive_native_context.py | 27 ++++++++++++ 13 files changed, 169 insertions(+), 63 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx index 008c412f41..db40e45e4c 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx @@ -282,7 +282,8 @@ export function MachineConfigurationSettings({ section, onChanged }: { async function setContextDelivery(mode: "off" | "on" | "auto", key: "mode" | "delta_delivery" = "mode") { if (busy || !editorAvailable || selected?.capability_id !== "context_delivery" || !selected.machine_namespace) return; const namespace = selected.machine_namespace; - const configuration = {...selected.default, ...selectedCurrent, [key]: mode}; + const configuration: Record = {...selected.default, ...selectedCurrent, + schema_version: selected.default?.schema_version ?? selectedCurrent?.schema_version, [key]: mode}; const generation = editorGeneration.current; setBusy("apply"); setError(null); setNotice(null); try { @@ -295,7 +296,8 @@ export function MachineConfigurationSettings({ section, onChanged }: { if (generation !== editorGeneration.current) return; setInspection(next); setReadbackPending(null); - if (next.machine_configuration?.namespaces[namespace]?.[key] !== mode) { + const saved = next.machine_configuration?.namespaces[namespace]; + if (saved?.mode !== configuration.mode || saved?.delta_delivery !== configuration.delta_delivery) { throw new Error(locale === "zh-CN" ? "保存后的配置已变化,请刷新后重试。" : "The saved configuration changed. Refresh and retry."); } const targetLabel = key === "delta_delivery" diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 337a2bb461..87f63b0fd1 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -580,10 +580,11 @@ revision-locked preview, apply, readback, disable and rollback owner. Read-only editors cannot save; sibling settings and Goal records remain unchanged. No choice installs hooks, trusts a host or proves actual delivery or model retention. -The existing `context_delivery_machine_defaults_v0` namespace accepts -`{"schema_version":"context_delivery_machine_defaults_v0","mode":"on","delta_delivery":"auto"}`. -`mode` and `antigravity_mode` keep their `on|off` values; omitted `delta_delivery` -defaults to `auto` and also accepts `off`. Old saved rows remain readable. +The editor saves `context_delivery_machine_defaults_v1` with both choices. +For example, `{"schema_version":"context_delivery_machine_defaults_v1","mode":"off","delta_delivery":"auto"}` +allows a supported host to compare complete documents without progressive presentation. +Existing v0 rows with `mode=off` retain their original full delivery, including rows +that stored an implicit `delta_delivery=auto`. Editing either switch adopts v1. `machine-config preview/apply --namespace context_delivery --config-json FILE` requires the original preview revision. Removing the namespace restores progressive reads, supported-host Delta AUTO and Antigravity manager history reads. Save the @@ -595,8 +596,9 @@ other declared hosts and recovery do not need native setup. A plain read-only `should-run` stays read-only: capture files do not create a Turn, receipt, session, journal or execution lease. `--delta-delivery off` independently disables Delta while retaining progressive reads. Delta AUTO only uses supported declared -continuity hosts. Explicit `--context-delivery off`, a saved off or an inherited -`LOOPX_CONTEXT_DELIVERY=off` keeps the original complete inline path. Environment +continuity hosts. A lone `--context-delivery off` or inherited `LOOPX_CONTEXT_DELIVERY=off` keeps +the original complete path. Explicit `--context-delivery off --delta-delivery auto` +requests cross-Turn reuse of the complete document when continuity is available. Environment on alone supplies no host, retention or execution proof. Private capture failure retains the complete current input rather than using an older capture. New benchmark heartbeat trials default to `context_delivery=on`; pass `off` to @@ -961,9 +963,10 @@ quota 能力候选沿用同一精确 codec 复用已登记 Todo 的正文、标 应用、读回、关闭和回滚 owner。只读编辑器不能保存;不改写其他配置和 Goal,不安装/信任 hook, 保存偏好不证明原生初始化、实际投递或模型保留。 -既有 `context_delivery_machine_defaults_v0` namespace 接受 -`{"schema_version":"context_delivery_machine_defaults_v0","mode":"on","delta_delivery":"auto"}`。 -`mode` 与 `antigravity_mode` 保留 `on|off`;可选的 `delta_delivery` 默认为 `auto`,也可为 `off`;旧配置继续可读。 +设置页以 `context_delivery_machine_defaults_v1` 同时保存两个选择。 +例如 `{"schema_version":"context_delivery_machine_defaults_v1","mode":"off","delta_delivery":"auto"}` +让支持的宿主比较完整文档,同时关闭渐进呈现。旧 v0 配置的 `mode=off` 保留原完整交付, +包括曾隐式存入 `delta_delivery=auto` 的记录。编辑任一开关才采用 v1。 CLI 的 `machine-config preview/apply --namespace context_delivery --config-json FILE` 仍需原预览修订。 移除 namespace 恢复渐进读取、支持宿主的 Delta AUTO 与 Antigravity 管家历史读取;要保持关闭应保存对应 off。 管家自动选择交付历史的读取方式。有权限读取私有快照的原生管家可按需读取已有历史,当前目标与待办仍直接可见。既有显式 `antigravity_mode=off` 偏好继续生效。 @@ -971,7 +974,8 @@ CLI 的 `machine-config preview/apply --namespace context_delivery --config-json CLI 的 `--context-delivery auto`(省略时也使用此门面)跨宿主先呈现当前要求,再按需展开材料。 普通 CLI、其他已声明宿主及恢复读取不要求原生初始化;只读 `should-run` 仍不创建 Turn、回执、会话、journal 或执行 lease。 `--delta-delivery off` 可独立关闭跨轮差分而保留渐进读取;Delta AUTO 只使用支持连续性的已声明宿主。 -显式 `--context-delivery off`、已保存的关闭及继承的 `LOOPX_CONTEXT_DELIVERY=off` 保留原完整内联路径。 +单独指定 `--context-delivery off` 或继承 `LOOPX_CONTEXT_DELIVERY=off` 保留原完整交付路径。 +同时指定 `--context-delivery off --delta-delivery auto` 时,有连续性依据的宿主可以复用完整文档的跨轮差分。 环境 on 不证明宿主、保留或执行权限。私有捕获失败时保留本次完整输入,不借用旧捕获。 新 benchmark heartbeat trial 默认 `context_delivery=on`,指定 `off` 可关闭;其他跑法仍关闭。 worker 把本次运行已解析的选择固定传给预检和 solver 子进程,不改写调用方原设置, diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index 506e90941f..61384612ab 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -225,9 +225,16 @@ export const configurationBackupScenario = { await contextSwitch.click(); await page.getByText("Progressive reads preference saved.", {exact: true}).waitFor(); const storedSettings = () => readFile(join(root, "runtime/machine/configuration.json"), "utf8").then(JSON.parse); - assert.equal((await storedSettings()).namespaces.context_delivery.mode, "off"); + const deltaOnly = (await storedSettings()).namespaces.context_delivery; + assert.deepEqual([deltaOnly.schema_version, deltaOnly.mode, deltaOnly.delta_delivery], + ["context_delivery_machine_defaults_v1", "off", "auto"]); contextSwitch = await openContextSettings(); assert.equal(await contextSwitch.isChecked(), false, "A saved off must survive reload and the new on default"); + const deltaSwitch = page.getByRole("switch", {name: "Use cross-turn delta automatically on supported hosts"}); + assert.equal(await deltaSwitch.isChecked(), true); + await deltaSwitch.click(); + await page.getByText("Cross-turn delta preference saved.", {exact: true}).waitFor(); + assert.equal(await contextSwitch.isChecked(), false); const applyRoute = "**/api/chat/machine-configuration/apply"; await page.route(applyRoute, route => route.fulfill({status: 503, contentType: "application/json", body: JSON.stringify({ok: false, error: "Fixture unavailable"})})); await contextSwitch.click(); @@ -237,7 +244,8 @@ export const configurationBackupScenario = { await contextSwitch.click(); await page.getByText("Progressive reads preference saved.", {exact: true}).waitFor(); const afterSettings = await storedSettings(); - assert.equal(afterSettings.namespaces.context_delivery.mode, "on"); + assert.deepEqual([afterSettings.namespaces.context_delivery.mode, afterSettings.namespaces.context_delivery.delta_delivery], + ["on", "off"]); assert.equal(afterSettings.namespaces.goal_storage.new_goal_provider, "sqlite", "The opt-out/on transaction must preserve sibling settings"); return {coverageEntries: context.coverageEntries, errors: context.errors, backend, verified: `Actual settings backend: Codex default on, saved off/reload, failed apply and explicit on; Packaged configuration recovery and full archive inspection; real ${backend.source_provider} head/history after original-source loss, no Goal registration, keyboard, corrupt-input recovery, immutable history reload and narrow viewport`}; } finally { diff --git a/loopx/capabilities/machine_configuration/context_delivery.py b/loopx/capabilities/machine_configuration/context_delivery.py index 3c588d7be5..eac8090c59 100644 --- a/loopx/capabilities/machine_configuration/context_delivery.py +++ b/loopx/capabilities/machine_configuration/context_delivery.py @@ -8,33 +8,43 @@ CONTEXT_DELIVERY_MACHINE_SCHEMA = "context_delivery_machine_defaults_v0" +INDEPENDENT_CONTEXT_SCHEMA = "context_delivery_machine_defaults_v1" def normalize_context_delivery_machine_defaults(raw: Mapping[str, Any]) -> dict[str, Any]: keys = set(raw) if not ({"schema_version", "mode"} <= keys <= {"schema_version", "mode", "delta_delivery", "antigravity_mode"}) or ( - raw.get("schema_version") != CONTEXT_DELIVERY_MACHINE_SCHEMA + raw.get("schema_version") not in (CONTEXT_DELIVERY_MACHINE_SCHEMA, INDEPENDENT_CONTEXT_SCHEMA) ): raise ValueError("context_delivery requires its schema_version and mode") + if raw["schema_version"] == INDEPENDENT_CONTEXT_SCHEMA and "delta_delivery" not in raw: + raise ValueError("independent context preferences require delta_delivery") antigravity_mode = raw.get("antigravity_mode", "on") delta = raw.get("delta_delivery", "auto") if (raw["mode"] not in ("off", "on") or antigravity_mode not in ("off", "on") or delta not in ("off", "auto")): raise ValueError("context_delivery mode and antigravity_mode must be off/on; delta_delivery off/auto") return { - "schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, + "schema_version": raw["schema_version"], "mode": raw["mode"], "delta_delivery": delta, **({"antigravity_mode": antigravity_mode} if "antigravity_mode" in raw else {}), } +def _public_context_preferences(value: Mapping[str, Any]) -> dict[str, Any]: + # Older editors saved an implicit auto even when the master switch was off. + if value["schema_version"] == CONTEXT_DELIVERY_MACHINE_SCHEMA and value["mode"] == "off": + return {**value, "delta_delivery": "off"} + return dict(value) + + def context_delivery_machine_configuration_namespace() -> MachineConfigurationNamespace: return MachineConfigurationNamespace( namespace="context_delivery", - schema_versions=frozenset({CONTEXT_DELIVERY_MACHINE_SCHEMA}), + schema_versions=frozenset({CONTEXT_DELIVERY_MACHINE_SCHEMA, INDEPENDENT_CONTEXT_SCHEMA}), normalize=normalize_context_delivery_machine_defaults, - project_public=lambda value: dict(value), + project_public=_public_context_preferences, apply_public_update=lambda current, update: {**dict(current or {}), **dict(update)}, title="Progressive context", description=( @@ -45,7 +55,7 @@ def context_delivery_machine_configuration_namespace() -> MachineConfigurationNa ), documentation={"path": "docs/reference/required-work-context.md#machine-default"}, default_configuration={ - "schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, + "schema_version": INDEPENDENT_CONTEXT_SCHEMA, "mode": "on", "delta_delivery": "auto", }, diff --git a/loopx/cli_commands/context_machine.py b/loopx/cli_commands/context_machine.py index cc6622bc15..f031a70fdb 100644 --- a/loopx/cli_commands/context_machine.py +++ b/loopx/cli_commands/context_machine.py @@ -78,13 +78,14 @@ def apply_context_machine_options( No hook, trust, tracing or automation is installed. """ explicit = getattr(args, "context_delivery", None) - if explicit == "off": + if explicit == "off" and getattr(args, "delta_delivery", None) != "auto": return args.context_delivery = explicit or "off" # Existing benchmark workers retain their explicit opt-out. A machine # default must not silently change a frozen worker's disabled transport. # An environment on alone still supplies no activation or host proof. if explicit is None and os.environ.get("LOOPX_CONTEXT_DELIVERY") == "off": + args.delta_delivery = "off" return command = getattr(args, "quota_command", None) if not heartbeat and command not in {"should-run", "context-config", "context-project", "context-hook", "context-read"}: @@ -108,12 +109,18 @@ def apply_context_machine_options( if using_default: value = machine_registry.resolve("context_delivery").public_descriptor()["configuration_template"] if value["mode"] == "off": - return + from ..capabilities.machine_configuration.context_delivery import INDEPENDENT_CONTEXT_SCHEMA + + if value["schema_version"] != INDEPENDENT_CONTEXT_SCHEMA: + args.delta_delivery = "off" + return # Presentation is host-neutral. Only Delta needs declared lifecycle # continuity. A read-only query must not manufacture an admitted Turn. - args.context_delivery = explicit or "auto" + args.context_delivery = explicit or ("auto" if value["mode"] == "on" else "off") if getattr(args, "delta_delivery", None) is None: args.delta_delivery = value.get("delta_delivery", "auto") + if args.context_delivery == "off" and args.delta_delivery == "off": + return if ((command == "should-run" and not (getattr(args, "turn_instance_id", None) or bool(getattr(args, "begin_turn", False)))) or os.name != "posix") and explicit != "on": args.delta_delivery = "off" diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index dbdabeacb9..ba9d417993 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -345,7 +345,7 @@ def reference(value: dict[str, Any], *, navigation: bool = False) -> dict[str, s def _native_context_reference(projected: dict[str, Any], reference: dict[str, Any], *, mode: str, current: dict[str, Any], current_document: dict[str, Any], total_bytes: int, metadata: dict[str, Any] | None, - field_index: list[dict[str, Any]] | None, + progressive_enabled: bool, field_index: list[dict[str, Any]] | None, initial_read_pointers: list[str] | None, page_bytes: int, output_bytes: int, reader_argv: list[str] | None = None) -> dict[str, Any]: @@ -376,7 +376,7 @@ def _native_context_reference(projected: dict[str, Any], reference: dict[str, An result["overview"] = {key: value for key, value in current_document.items() if value is None or isinstance(value, (str, bool, int, float)) and len(json.dumps(value, ensure_ascii=False).encode("utf-8")) <= 240} - result["instruction"] = CONTEXT_CAPTURE_NAVIGATION_GUIDANCE + result["instruction"] = CONTEXT_CAPTURE_NAVIGATION_GUIDANCE if progressive_enabled else CONTEXT_CAPTURE_READ_GUIDANCE if mode != "full": result["instruction"] += (" command reads the delta; baseline_reference identifies its exact saved base when file-backed. " "recovery.command reads current facts directly without that baseline.") @@ -457,7 +457,7 @@ def _native_context_read_bytes(reference: dict[str, Any], raw: bytes, *, def native_context_capture_view(projected: dict[str, Any], directory: Path, *, context_directory: str | None = None, - hook_session: str | None = None, + progressive_enabled: bool = True, hook_session: str | None = None, captured_recovery: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None, max_page_output_bytes: int | None = None, @@ -496,19 +496,21 @@ def native_context_capture_view(projected: dict[str, Any], directory: Path, *, # once for current facts, not once per full/delta transport candidate. field_index = None initial_read_pointers = None - try: - plan = _turn_envelope_result("context_select", context=current_document, select=[""], list_fields=True) - except (EffectRuntimeRemoteError, EffectRuntimeStartupError): - # Optional discovery cannot invalidate the immutable file reference. - pass - else: - field_index = _context_index_fields(current_document, plan["selections"][0]["children"]) - initial_read_pointers = plan.get("initial_read_pointers") + if progressive_enabled: + try: + plan = _turn_envelope_result("context_select", context=current_document, select=[""], list_fields=True) + except (EffectRuntimeRemoteError, EffectRuntimeStartupError): + # Optional discovery cannot invalidate the immutable file reference. + pass + else: + field_index = _context_index_fields(current_document, plan["selections"][0]["children"]) + initial_read_pointers = plan.get("initial_read_pointers") def view(kind: str, ref: dict[str, Any], body: bytes) -> dict[str, Any]: return _native_context_reference(projected, ref, mode=kind, current=current, current_document=current_document, total_bytes=len(body), metadata=metadata, field_index=field_index, initial_read_pointers=initial_read_pointers, + progressive_enabled=progressive_enabled, page_bytes=page_bytes, output_bytes=output_bytes, reader_argv=reader_argv) if typed and mode == "delta": @@ -569,7 +571,7 @@ def project_native_quota_context(payload: dict[str, Any], args: argparse.Namespa capture_id=capture_id, continuity_generation=getattr(args, "_context_delivery_generation", None), progressive_enabled=progressive_enabled) recovery = payload.get("recovery") - view = native_context_capture_view(payload, capture_directory, context_directory=directory, + view = native_context_capture_view(payload, capture_directory, context_directory=directory, progressive_enabled=progressive_enabled, metadata={"capture_sha256": capture_id, **({"recovery": recovery} if recovery is not None else {})}, max_page_output_bytes=getattr(args, "max_page_output_bytes", None), reader_argv=reader_argv) return model_context_reference(view, reader_argv=reader_argv) @@ -711,6 +713,7 @@ def project_saved_context_capture(args: argparse.Namespace, *, hook_session: str page_bytes=page_bytes, output_bytes=output_bytes, reader_argv=reader_argv) reference = dict(full_reference) native_view = native_context_capture_view(projected, cache, context_directory=directory, hook_session=hook_session, + progressive_enabled=activation["progressive_enabled"], metadata={"recovery": full_reference, "capture_sha256": args.capture_sha256, "context_delivery": activation}, max_page_output_bytes=getattr(args, "max_page_output_bytes", None), reader_argv=reader_argv) @@ -1050,7 +1053,7 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca "or enabled native should-run/context-project, or Turn-bound JSON spend-slot") if has_output_limit and args.quota_command != "context-read" and not settlement_output and ( args.quota_command not in ("should-run", "context-project") or args.format != "json" - or getattr(args, "context_delivery", None) == "off"): + or (getattr(args, "context_delivery", None) == "off" and getattr(args, "delta_delivery", None) != "auto")): print_payload({"ok": False, "reason": budget_scope_error}, args.format, lambda value: str(value["reason"])) return 1 @@ -1103,10 +1106,16 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca if not directory: raise ValueError("context-config requires codex-context-root or codex-context-dir") machine_runtime = getattr(args, "_context_delivery_machine_runtime", None) - cli_argv = loopx_cli_argv() + (["--runtime-root", machine_runtime] if machine_runtime else []) + cli_argv = loopx_cli_argv() + if registry_path is not None: + cli_argv += ["--registry", str(registry_path.expanduser().resolve())] + hook_runtime = machine_runtime or runtime_root_arg + if hook_runtime: + cli_argv += ["--runtime-root", str(Path(hook_runtime).expanduser().resolve())] config = codex_context_configuration(directory, goal_id=args.goal_id, agent_id=args.agent_id, cli_argv=cli_argv, scenario=activation["scenario"], session_root=bool(getattr(args, "codex_context_root", None)), - inherit_machine=bool(machine_runtime)) + inherit_machine=bool(machine_runtime), progressive_enabled=activation["progressive_enabled"]) + explicit_options = "--context-delivery on" if activation["progressive_enabled"] else "--context-delivery off --delta-delivery auto" payload = {"ok": True, "context_delivery": activation, "native_config": config, "activation": "Apply config_overrides, merge hooks and append developer_instructions to the existing native configuration; " "review and trust only these exact hooks in Codex. " @@ -1115,7 +1124,7 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca "with the resulting configuration after active work reaches its normal safe boundary. " + ("Machine defaults are re-read on each invocation; generated hooks inherit the same machine runtime. " "Omit per-command activation and location flags to retain that inheritance. " if machine_runtime else - "Pass --context-delivery on with the same declared host mode, context location and a decision-output-root to heartbeat-prompt/should-run. ") + f"Pass {explicit_options} with the same declared host mode, context location and a decision-output-root to heartbeat-prompt/should-run. ") + "Verified saved files support progressive reads and exact deltas without a full model read; compaction starts a new full baseline on the next work Turn. " "The generated output profile uses 64000-byte continuations and a 20000-token native history limit; " diff --git a/loopx/cli_commands/quota_registration.py b/loopx/cli_commands/quota_registration.py index f88a0f6435..7c9636b08b 100644 --- a/loopx/cli_commands/quota_registration.py +++ b/loopx/cli_commands/quota_registration.py @@ -220,7 +220,7 @@ def register_quota_command( from ..control_plane.turn_driver.codex_context import CONTEXT_DELIVERY_SETTINGS, DELTA_DELIVERY_SETTINGS quota_parser.add_argument("--context-delivery", choices=CONTEXT_DELIVERY_SETTINGS, - help="Auto defaults to progressive current-context reads on every host, with Delta on supported continuity hosts. Explicit off keeps the original inline path. Legacy on uses existing host declarations: " + help="Auto defaults to progressive current-context reads on every host, with Delta on supported continuity hosts. Off alone keeps full delivery. Pair off with --delta-delivery auto to compare complete documents. Legacy 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. " "Enabled supported calls choose private context/capture paths automatically unless overridden. A directory alone never enables it; declarations are not retention proof.") diff --git a/loopx/cli_commands/support_control_heartbeat.py b/loopx/cli_commands/support_control_heartbeat.py index 1d558b7282..4944186505 100644 --- a/loopx/cli_commands/support_control_heartbeat.py +++ b/loopx/cli_commands/support_control_heartbeat.py @@ -170,7 +170,7 @@ def handle_heartbeat_prompt_command( codex_context_dir=args.codex_context_dir, codex_context_root=args.codex_context_root, context_delivery=args.context_delivery, - delta_delivery=args.delta_delivery or "auto", + delta_delivery=args.delta_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 09c1749d65..c73a6be884 100644 --- a/loopx/cli_commands/support_control_heartbeat_registration.py +++ b/loopx/cli_commands/support_control_heartbeat_registration.py @@ -183,12 +183,12 @@ def register_heartbeat_control_commands( from ..control_plane.turn_driver.codex_context import CONTEXT_DELIVERY_SETTINGS, DELTA_DELIVERY_SETTINGS heartbeat_prompt_parser.add_argument("--context-delivery", choices=CONTEXT_DELIVERY_SETTINGS, - help="Auto presents context progressively across hosts, with independent Delta on supported continuity hosts. Explicit off preserves complete inline delivery. Legacy on uses existing declarations: " + help="Auto presents context progressively across hosts, with independent Delta on supported continuity hosts. Off alone keeps full delivery. Pair off with --delta-delivery auto to compare complete documents. Legacy on uses existing 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. " "Enabled supported calls choose private context/capture paths automatically unless overridden. A directory alone never enables it; declarations are not retention proof.") heartbeat_prompt_parser.add_argument("--delta-delivery", choices=DELTA_DELIVERY_SETTINGS, - help="Independently disable cross-Turn Delta or inherit supported-host automatic reuse. Progressive reads remain enabled.") + help="Independently disable cross-Turn Delta or inherit supported-host automatic reuse. This choice is independent of progressive presentation.") context_location = heartbeat_prompt_parser.add_mutually_exclusive_group() context_location.add_argument( "--codex-context-dir", diff --git a/loopx/control_plane/heartbeat/builder.py b/loopx/control_plane/heartbeat/builder.py index 5c0a03fb5a..6a7cf276cd 100644 --- a/loopx/control_plane/heartbeat/builder.py +++ b/loopx/control_plane/heartbeat/builder.py @@ -180,7 +180,7 @@ def _heartbeat_prompt_commands( codex_context_root: str | None, context_delivery: str | None, turn_envelope: bool, - delta_delivery: str = "auto", + delta_delivery: str | None = None, ) -> dict[str, str | None]: quota_guard_command = render_quota_guard_command( goal_id, @@ -293,7 +293,7 @@ def _heartbeat_prompt_commands( capture_arg += " --codex-context-root " + shlex.quote(codex_context_root) elif context_delivery is not None: capture_arg += " --context-delivery " + shlex.quote(context_delivery) - if delta_delivery != "auto": + if delta_delivery == "off" or (context_delivery == "off" and delta_delivery == "auto"): capture_arg += " --delta-delivery " + shlex.quote(delta_delivery) if capture_arg: display_arg = " --turn-envelope" if turn_envelope else "" @@ -303,7 +303,7 @@ def _heartbeat_prompt_commands( return commands -def _heartbeat_context_capture_activation(*, context_delivery: str | None, delta_delivery: str, +def _heartbeat_context_capture_activation(*, context_delivery: str | None, delta_delivery: str | None, runtime_profile: str | None, scheduler_execution_context: Any, decision_output_root: Path | None, decision_handoff_socket: str | None, codex_context_dir: str | None, codex_context_root: str | None) -> tuple[dict[str, Any], str, str | None, str | None]: @@ -354,7 +354,7 @@ def build_heartbeat_prompt( codex_context_dir: str | None = None, codex_context_root: str | None = None, context_delivery: str | None = None, - delta_delivery: str = "auto", + delta_delivery: str | None = None, turn_envelope: bool = False, ) -> dict[str, Any]: if not (full or compact or brief or thin): diff --git a/loopx/control_plane/turn_driver/codex_context.py b/loopx/control_plane/turn_driver/codex_context.py index 99a3f9c84b..58dd0f629f 100644 --- a/loopx/control_plane/turn_driver/codex_context.py +++ b/loopx/control_plane/turn_driver/codex_context.py @@ -55,7 +55,7 @@ def context_delivery_for_arguments(args: Any, *, codex_machine_preference: bool "antigravity_app" if getattr(args, "antigravity_app", False) else None) if value is not None] setting = getattr(args, "context_delivery", None) setting = ("off" if os.environ.get("LOOPX_CONTEXT_DELIVERY") == "off" else "auto") if setting is None else setting - delta = getattr(args, "delta_delivery", None) or "auto" + delta = getattr(args, "delta_delivery", None) if len(profiles) > 1: return context_delivery_activation(setting, delta_delivery=delta) return context_delivery_activation(setting, runtime_profile=profiles[0] if profiles else None, @@ -69,7 +69,7 @@ def context_delivery_activation(setting: str, *, runtime_profile: str | None = N scheduler_execution_context: dict[str, Any] | None = None, benchmark_mode: str | None = None, codex_machine_preference: bool = False, - delta_delivery: str = "auto") -> dict[str, Any]: + delta_delivery: str | None = None) -> dict[str, Any]: from ..scheduler.execution_context import ( ExecutionMode, HostSurface, SchedulerOwner, resolve_scheduler_execution_context, scheduler_execution_context_for_runtime_profile, @@ -77,10 +77,12 @@ def context_delivery_activation(setting: str, *, runtime_profile: str | None = N if setting not in CONTEXT_DELIVERY_SETTINGS: raise ValueError("context-delivery must be off, on or auto") - if delta_delivery not in DELTA_DELIVERY_SETTINGS: + if delta_delivery is not None and delta_delivery not in DELTA_DELIVERY_SETTINGS: raise ValueError("delta-delivery must be off or auto") scenario = None - if setting != "off" and delta_delivery != "off": + # A lone legacy off is raw. An explicit pair can qualify Delta on the + # full document without opting into progressive presentation. + if delta_delivery == "auto" or (delta_delivery is None and setting != "off"): # 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: @@ -283,7 +285,7 @@ def codex_context_recovery_instructions(directory: str, *, goal_id: str, agent_i def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, cli_argv: list[str], scenario: str, session_root: bool = False, - inherit_machine: bool = False) -> dict[str, Any]: + inherit_machine: bool = False, progressive_enabled: bool = True) -> 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 @@ -298,9 +300,11 @@ def codex_context_configuration(directory: str, *, goal_id: str, agent_id: str, 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" + activation_args = ["--context-delivery", "on"] if progressive_enabled else [ + "--context-delivery", "off", "--delta-delivery", "auto"] handler = {"type": "command", "command": shlex.join([ *cli_argv, "--format", "json", "quota", "context-hook", - *([] if inherit_machine else ["--context-delivery", "on"]), + *([] if inherit_machine else activation_args), *(["--codex-app"] if scenario == "codex_app_heartbeat" else ["--runtime-profile", "codex_app_ssh_goal" if scenario == "codex_app_ssh_goal" else "generic_cli"]), option, directory]), "timeout": 10} @@ -597,7 +601,7 @@ def register_native_context_view(directory: str | None, view: dict[str, Any], *, 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, continuity_generation: str | None = None, - progressive_enabled: bool = False, hook_session: str | None = None) -> dict[str, Any]: + progressive_enabled: bool | None = None, hook_session: str | None = None) -> dict[str, Any]: """Commit current recovery before optional projection or candidate updates. Serialization prevents invalidation from moving the epoch during preparation. @@ -661,6 +665,8 @@ def publish_recovery(reference: dict[str, Any]) -> None: # or encoding can be interrupted. Never recover its predecessor. publish_recovery(recovery) recovery_committed = True + # Legacy direct native callers omitted the presentation option. + progressive = progressive_enabled is not False try: transaction = prepare_context_delivery(payload, scope=scope, capture_id=capture_id, surface=surface, baseline=request.get("baseline"), @@ -671,14 +677,15 @@ def publish_recovery(reference: dict[str, Any]) -> None: "command_template": context_capture_read_command(Path(recovery["path"]), recovery["sha256"], page_bytes=page_bytes, max_page_output_bytes=output_bytes) + " --select '/sources'", - }, consumed_pointers=state.get("consumed_pointers")) + } if progressive else None, consumed_pointers=state.get("consumed_pointers")) except (EffectRuntimeRemoteError, EffectRuntimeStartupError): return payload - if transaction["projected"]: + if progressive and transaction["projected"]: recovery = capture_context_projection(transaction["context"], directory=capture_directory, filename="required-context.json", read_guidance=CONTEXT_CAPTURE_PAGE_GUIDANCE) publish_recovery(recovery) - result: dict[str, Any] = transaction["delivery"] + result: dict[str, Any] = transaction["delivery"] if progressive else transaction + read_sections = transaction["read_sections"] if progressive else {} result["recovery"] = recovery if result.get("continuity_basis") == "captured_file" and result.get("mode") == "delta": result["baseline_reference"] = request["baseline_reference"] @@ -686,8 +693,8 @@ def publish_recovery(reference: dict[str, Any]) -> None: state["recovery"]["target"] = result["target"] state["recovery"]["continuity_basis"] = "captured_file" state["pending"].update(target=result["target"], ancestry=result["ancestry"], - full_read_sections=transaction["read_sections"], - read_sections=transaction["read_sections"] if result["mode"] == "full" else {}) + full_read_sections=read_sections, + read_sections=read_sections if result["mode"] == "full" else {}) _save(root, state) return result except (OSError, ValueError, KeyError, TypeError, RecursionError): diff --git a/tests/control_plane/test_context_machine_configuration.py b/tests/control_plane/test_context_machine_configuration.py index c633e15bc1..9956caeae3 100644 --- a/tests/control_plane/test_context_machine_configuration.py +++ b/tests/control_plane/test_context_machine_configuration.py @@ -12,7 +12,9 @@ import pytest -from loopx.capabilities.machine_configuration.context_delivery import CONTEXT_DELIVERY_MACHINE_SCHEMA +from loopx.capabilities.machine_configuration.context_delivery import ( + CONTEXT_DELIVERY_MACHINE_SCHEMA, INDEPENDENT_CONTEXT_SCHEMA, +) from loopx.control_plane.turn_driver.decision_capture import read_context_read_reference from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime from tests.control_plane.test_quota_settlement_cli import ( @@ -57,9 +59,12 @@ def test_rejected_context_snapshot_uses_private_generic_transport_without_native assert read_context_capture_file(result["path"], result["sha256"], private=True)[2] == value assert sorted(p.name for p in root.iterdir()) == sorted([path.parent.name, Path(other["path"]).parent.name]) -def configure(registry, runtime, root, mode): +def configure(registry, runtime, root, mode, *, delta=None): config = root / "context-setting.json" - config.write_text(json.dumps({"schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, "mode": mode})) + value = {"schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, "mode": mode} + if delta is not None: + value.update(schema_version=INDEPENDENT_CONTEXT_SCHEMA, delta_delivery=delta) + config.write_text(json.dumps(value)) args = ["--namespace", "context_delivery", "--config-json", str(config)] code, preview = _run_cli(registry, runtime, "machine-config", "preview", *args) if code: @@ -74,9 +79,9 @@ def test_machine_catalog_discovers_context_delivery_without_activation(tmp_path) assert code == 0 and catalog["schema_version"] == "machine_configuration_catalog_v0", catalog contexts = [item for item in catalog["namespaces"] if item["namespace"] == "context_delivery"] assert len(contexts) == 1 - assert contexts[0]["schema_versions"] == [CONTEXT_DELIVERY_MACHINE_SCHEMA] + assert contexts[0]["schema_versions"] == [CONTEXT_DELIVERY_MACHINE_SCHEMA, INDEPENDENT_CONTEXT_SCHEMA] assert contexts[0]["configuration_template"] == { - "schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, "mode": "on", "delta_delivery": "auto", + "schema_version": INDEPENDENT_CONTEXT_SCHEMA, "mode": "on", "delta_delivery": "auto", } assert not registry.exists() and not (runtime / "machine/configuration.json").exists() assert not (runtime / "context-delivery").exists() @@ -117,6 +122,33 @@ def test_invalid_machine_mode_never_writes(tmp_path, invalid): assert not registry.exists() and not (runtime / "machine/configuration.json").exists() +@pytest.mark.skipif(os.name != "posix", reason="Native session continuity requires POSIX") +@pytest.mark.parametrize("mode", ["off", "on"]) +@pytest.mark.parametrize("delta", ["off", "auto"]) +def test_independent_machine_switches_and_explicit_cli_agree(tmp_path, monkeypatch, mode, delta): + registry, runtime = tmp_path / "absent.json", tmp_path / "runtime" + args = ["quota", "context-config", "--codex-app", "--goal-id", "fixture", "--agent-id", "worker"] + code, explicit = _run_cli(registry, runtime, *args, "--context-delivery", mode, "--delta-delivery", delta) + assert code == 0, explicit + assert configure(registry, runtime, tmp_path, mode, delta=delta)[1]["readback_verified"] + code, saved = _run_cli(registry, runtime, *args) + assert code == 0, saved + for result in (explicit, saved): + activation = result["context_delivery"] + assert activation["progressive_enabled"] is (mode == "on") + assert activation["enabled"] is (delta == "auto") + assert activation["delta_delivery"] == delta + if mode == "off" and delta == "auto": + command = shlex.split(explicit["native_config"]["hooks"]["SessionStart"][0]["hooks"][0]["command"]) + assert command[command.index("--context-delivery") + 1] == "off" + assert command[command.index("--delta-delivery") + 1] == "auto" + assert command[command.index("--registry") + 1] == str(registry) + assert command[command.index("--runtime-root") + 1] == str(runtime) + monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "off") + code, inherited = _run_cli(registry, runtime, *args) + assert code == 0 and not inherited["context_delivery"]["enabled"] + + def test_machine_on_preserves_existing_worker_environment_off(tmp_path, monkeypatch): registry, runtime = tmp_path / "absent.json", tmp_path / "runtime" assert configure(registry, runtime, tmp_path, "on")[0] == 0 diff --git a/tests/control_plane/test_progressive_native_context.py b/tests/control_plane/test_progressive_native_context.py index 7eaaf16620..94ba08193e 100644 --- a/tests/control_plane/test_progressive_native_context.py +++ b/tests/control_plane/test_progressive_native_context.py @@ -337,6 +337,33 @@ def test_small_material_keeps_full_capture_when_the_detail_index_costs_more(tmp_ assert not Path(recovery["path"]).with_name("required-context.json").exists() +def test_delta_only_retains_original_material_and_recovers_changes(tmp_path, monkeypatch): + from loopx.control_plane.quota.turn_envelope import restore_context_delivery + + event = setup(tmp_path, monkeypatch) + previous = None + for turn, stop in enumerate(("Preserve scope.", "Stop now.")): + source = {**payload(), "stop": stop, "evidence": ["stable material " * 1000]} + capture = tmp_path / str(turn) + capture.mkdir(mode=0o700) + wire = context.project_codex_context(source, directory=str(tmp_path), capture_directory=capture, + goal_id="fixture", agent_id="agent", surface="quota_packet", capture_id=str(turn), + progressive_enabled=False) + assert json.loads(Path(wire["recovery"]["path"]).read_bytes()) == source + from loopx.cli_commands.quota_capture import native_context_capture_view + view = native_context_capture_view(wire, capture, context_directory=str(tmp_path), progressive_enabled=False) + assert "first_read_command" not in view and "Follow next_command through eof" in view["instruction"] + if previous is None: + assert wire["mode"] == "full" and wire["context"] == source + previous = wire + deliver(tmp_path, event, read_pages(tmp_path, wire)) + else: + assert wire["mode"] == "delta" + restored = restore_context_delivery(wire, baseline=previous, current={ + "schema_version": wire["schema_version"], "mode": "full", "target": wire["target"], "context": source}) + assert restored["mode"] == "delta" and restored["context"] == source + + def test_small_duplicate_rows_do_not_gain_reference_overhead(tmp_path, monkeypatch): setup(tmp_path, monkeypatch) row = {"schema_version": "todo_item_v0", "todo_id": "task", "text": "Keep this constraint.", "claimed_by": "peer"}