Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
fe80385
fix(replan): review structured state before captured writeback guidance
loopx-agent Oct 24, 2026
2035dfc
docs(replan): qualify staged reads and incremental context budgets
loopx-agent Oct 24, 2026
c18abc4
fix(replan): preserve reader routes and remove prefabricated choices
loopx-agent Oct 24, 2026
7e64594
refactor(quota): keep captured replan IO out of live dispatch
loopx-agent Oct 24, 2026
ed5c2bf
fix(checkpoint): recheck captured session without writing launch locks
loopx-agent Oct 23, 2026
e98020e
fix(worker): retain original replan across bounded host retry
loopx-agent Oct 23, 2026
b5bd8bc
Merge remote-tracking branch 'origin/main' into codex/replan-context-…
loopx-agent Oct 24, 2026
4a53fb7
fix(replan): replace repeated guidance and deliver captured checkpoin…
loopx-agent Oct 24, 2026
e16bc33
Merge remote-tracking branch 'origin/main' into codex/replan-context-…
loopx-agent Oct 24, 2026
0a79186
Merge remote-tracking branch 'origin/codex/edgebench-integration-v23-…
loopx-agent Oct 24, 2026
cda47ab
fix(checkpoint): admit explicit destination model on full continuation
loopx-agent Oct 24, 2026
71f673b
fix(checkpoint): admit explicit destination model on full continuation
loopx-agent Oct 24, 2026
b093522
fix(checkpoint): type explicit model transition at existing boundaries
loopx-agent Oct 24, 2026
135e723
Merge original checkpoint proposal ancestry after transplanting its f…
loopx-agent Oct 24, 2026
9772b95
Merge commit '62fb69d01131371bd9bfb75b090a2ad332a76c0e' into codex/re…
loopx-agent Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,31 @@ Fresh credentials come from the ordinary runner credential source. The native
activation's absolute deadline includes receiver installation and preparation;
restarting the entry cannot grant another full budget or submission allowance.

To continue with a different model or reasoning effort, retain the exact sealed
source settings in `--restore-config` and explicitly pass
`--restore-model-config DESTINATION_CONFIG`. Only the top-level `model` and
`model_reasoning_effort` may differ; provider, tools, permissions and other
settings must remain semantically identical. The destination model/effort must
also match `--model`/`--effort`. The native session participant records both
configuration digests, verifies the original session profile and full retained
history, then binds the new profile to a distinct destination session. Retry
uses that same source/destination intent; it cannot switch an already running
receiver. Without the option, exact-configuration admission is unchanged.
The host controller exposes the same explicit transition as
`--destination-model-config`; this remains a CLI/Host journey, with frontend
restore support still open. Changing the model changes the experiment treatment:
do not claim a matched mechanism comparison solely from shared checkpoint data.
It grants no extra native submissions, cooldown relief, permission or time.

切换模型或思考深度时,`--restore-config` 仍传封存源配置,另显式传入
`--restore-model-config DESTINATION_CONFIG`。仅顶层 `model` 与
`model_reasoning_effort` 可变化,其余 provider、工具、权限及配置语义保持一致,
目标选择还须匹配 `--model`/`--effort`。接收端保留源配置摘要和完整历史校验,
在全新会话绑定目标配置;恢复重试不得换意图或改写已运行接收端。省略选项仍要求
配置完全一致,不增加评测机会、豁免冷却、权限或预算。宿主 CLI 对应选项为
`--destination-model-config`;frontend restore 旅程仍未交付。模型变化是新的实验因素,
共享 checkpoint 本身不能证明机制收益。

Warm verification hashes the complete checkpoint with a read window of up to
60 seconds, capped by the original execution deadline. It rejects a read that
finishes after that deadline. The launcher reuses this verified metadata before
Expand Down
14 changes: 13 additions & 1 deletion benchmark/edgebench/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ def main(argv=None):
retention.add_argument("--no-checkpoint-retention", action="store_true",
help="Keep every captured point; disable default checkpoint rotation")
parser.add_argument("--restore-config", type=Path)
parser.add_argument("--restore-model-config", type=Path,
help="Explicit destination config for changing only model/effort in a new checkpoint receiver; --restore-config retains the sealed source config")
parser.add_argument("--restore-goal")
parser.add_argument("--restore-agent")
parser.add_argument("--restore-source-todo")
Expand Down Expand Up @@ -232,6 +234,8 @@ def main(argv=None):
parser.error("Research component restoration requires an explicit warm checkpoint continuation")
if args.restore_fresh_replan and not args.restore_checkpoint:
parser.error("Fresh context requires an explicit warm checkpoint continuation")
if args.restore_model_config is not None and not args.restore_checkpoint:
parser.error("Model transition requires an explicit warm checkpoint continuation")
if args.restore_checkpoint and (not handoff or args.eval_interval != 0 or args.replan_after_todos is not None):
parser.error("Warm continuation requires full native checkpoint capture, no periodic evaluation and effective-Turn cadence")
# One directory is one attempt: never reuse native registration or overwrite
Expand Down Expand Up @@ -311,8 +315,16 @@ def main(argv=None):
candidate = WarmStart(args.restore_checkpoint, args.restore_sha256, args.restore_config,
args.restore_goal, args.restore_agent, args.restore_source_todo,
int(time.time()) + args.timeout, args.replan_after_turns or 6,
tuple(args.restore_research_component), args.restore_fresh_replan)
tuple(args.restore_research_component), args.restore_fresh_replan,
destination_config=args.restore_model_config)
metadata = candidate.verify(task.cwd)
if args.restore_model_config is not None:
from loopx.hosts.codex_checkpoint import checkpoint_destination_config
_, transition = checkpoint_destination_config(args.restore_config,
metadata["native_session"]["config_sha256"], args.restore_model_config)
if (transition["destination_model"] != args.model or
transition["destination_effort"] != args.effort):
raise ValueError("Checkpoint destination model must match the explicit runner model/effort")
snapshot = metadata["worker_file_snapshot"]
capsule = args.restore_checkpoint / "components" / snapshot["native_capsule_component_id"]
clock = attempt / "restored-phase-deadline"
Expand Down
26 changes: 19 additions & 7 deletions benchmark/runtime/sforge_warm_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class WarmStart:
normal_cadence: int = 6
research_component_ids: tuple[str, ...] = ()
fresh_replan: bool = False
destination_config: Path | None = None

def verify(self, workspace: str) -> dict:
from loopx.hosts.checkpoint_retention import checkpoint_bank_use
Expand All @@ -60,8 +61,10 @@ def _verify(self, workspace: str) -> dict:
or any(not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", v)
for v in (self.goal_id, self.agent_id, self.source_todo_id))):
raise ValueError("Warm start needs fresh explicit identities and its original open deadline")
if any(not path.is_absolute() or path.resolve(strict=True) != path
for path in (self.root, self.config)):
paths = (self.root, self.config)
if self.destination_config is not None:
paths += (self.destination_config,)
if any(not path.is_absolute() or path.resolve(strict=True) != path for path in paths):
raise ValueError("Warm start requires physical absolute participant paths")
# Hashing a complete checkpoint can exceed the ordinary RPC budget.
# This read allowance never extends the original execution deadline.
Expand All @@ -74,9 +77,10 @@ def _verify(self, workspace: str) -> dict:
metadata = json.loads((self.root / "checkpoint.json").read_bytes())["point_metadata"]
native, source = metadata["native_session"], metadata["loopx_state"]
if (native["guest_workspace"] != workspace or native["guest_home"] != _CODEX_HOME
or source["goal_id"] == self.goal_id or source["agent_id"] == self.agent_id
or hashlib.sha256(self.config.read_bytes()).hexdigest() != native["config_sha256"]):
or source["goal_id"] == self.goal_id or source["agent_id"] == self.agent_id):
raise ValueError("Warm start paths, configuration or destination lineage differ")
from loopx.hosts.codex_checkpoint import checkpoint_destination_config
checkpoint_destination_config(self.config, native["config_sha256"], self.destination_config)
_workspace_component(self.root, metadata, Path(workspace))
_research_components(self.root, metadata, Path(workspace), self.research_component_ids)
context = metadata["research_context"]
Expand Down Expand Up @@ -110,6 +114,12 @@ async def _stage(self, environment, workspace: str) -> list[str]:
goal_id=self.goal_id, agent_id=self.agent_id,
source_todo_id=self.source_todo_id, deadline_epoch=self.deadline_epoch,
normal_cadence=self.normal_cadence)
protected_files = [config, f"{_CONTROL}/warm-start.json"]
if self.destination_config is not None:
target = f"{_CONTROL}/restored-model-config.toml"
await environment.upload_file(self.destination_config, target)
payload["destination_config"] = target
protected_files.append(target)
if self.research_component_ids:
payload["research_component_ids"] = list(self.research_component_ids)
if self.fresh_replan:
Expand All @@ -118,12 +128,12 @@ async def _stage(self, environment, workspace: str) -> list[str]:
path = Path(folder) / "input.json"
path.write_text(json.dumps(payload))
await environment.upload_file(path, f"{_CONTROL}/warm-start.json")
protected = shlex.join(protected_files)
result = await environment.exec(
# Modes are part of the sealed digest. Change transport ownership,
# never widen modes (native stores commonly contain 0600 members).
f"chown -R -P agent:agent {shlex.quote(unit)} && "
f"chown agent:agent {config} {_CONTROL}/warm-start.json && "
f"chmod 0400 {config} {_CONTROL}/warm-start.json", user="root")
f"chown agent:agent {protected} && chmod 0400 {protected}", user="root")
if result.return_code:
raise RuntimeError("Could not stage the verified checkpoint participants")
# The root-only materializer preserves sealed modes. Its receiver must
Expand Down Expand Up @@ -290,7 +300,9 @@ def admit_warm_session(env: dict[str, str], declaration: dict) -> dict:

timeout = _phase_read_timeout(declaration, 90)
result = admit_native_checkpoint_session(env, Path(declaration["root"]), declaration["sha256"],
Path(declaration["config"]), sandbox=env["LOOPX_CODEX_SANDBOX"], timeout=timeout)
Path(declaration["config"]), sandbox=env["LOOPX_CODEX_SANDBOX"], timeout=timeout,
**({"destination_config": Path(declaration["destination_config"])}
if declaration.get("destination_config") is not None else {}))
_phase_read_timeout(declaration, 90)
return result

Expand Down
39 changes: 32 additions & 7 deletions benchmark/tests/test_checkpoint_binding_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

@pytest.mark.parametrize("replay", [False, True])
@pytest.mark.parametrize("change_goal", [False, True])
@pytest.mark.parametrize("model_transition", [False, True])
def test_checkpoint_binding_and_readback_recheck_destination(
tmp_path, monkeypatch, capsys, replay, change_goal,
tmp_path, monkeypatch, capsys, replay, change_goal, model_transition,
):
from loopx.cli import main
from loopx.control_plane import effect_runtime
Expand All @@ -25,7 +26,9 @@ def test_checkpoint_binding_and_readback_recheck_destination(
for path in (workspace, home, unit):
path.mkdir()
config = home / "config.toml"
config.write_text("[features]\ngoals=false\n")
config.write_text('model="synthetic"\nmodel_reasoning_effort="high"\n[features]\ngoals=false\n')
source_config = tmp_path / "source.toml"
source_config.write_bytes(config.read_bytes())
prefix = ["--format", "json", "--registry", str(registry), "--runtime-root", str(runtime)]

def cli(*args):
Expand Down Expand Up @@ -53,11 +56,24 @@ def cli(*args):
(unit / "checkpoint.json").write_text(json.dumps({"point_metadata": {
"session": {"session_profile_digest": prepared.digest}, "native_session": native,
"loopx_state": {"goal_id": "source", "agent_id": "source-agent"}}}))
destination = None
if model_transition:
destination = tmp_path / "destination.toml"
destination.write_text(source_config.read_text().replace('"synthetic"', '"synthetic-next"')
.replace('"high"', '"max"'))
config.write_bytes(destination.read_bytes())
env.update(MODEL_NAME="synthetic-next", REASONING_EFFORT="max")
destination_profile = session_type(env, context="resume", sandbox="read-only", receipt={}).digest
transition = None
if model_transition:
transition = {**codex_checkpoint.checkpoint_destination_config(
source_config, native["config_sha256"], destination)[1],
"source_profile_digest": prepared.digest, "destination_profile_digest": destination_profile}
if replay:
_store_codex_cli_session(runtime, lineage=prepared.lineage, session_scope="agent",
session_id="destination-session", goal_ref=prepared.goal_ref,
session_profile_digest=prepared.digest, checkpoint_artifact_sha256=digest,
checkpoint_artifact_root=str(unit))
session_profile_digest=destination_profile, checkpoint_artifact_sha256=digest,
checkpoint_artifact_root=str(unit), checkpoint_model_transition=transition)
before = select_codex_cli_session(runtime, lineage=prepared.lineage, session_scope="agent")

# Native storage/inference is outside this lock regression. Registry,
Expand All @@ -74,6 +90,7 @@ def verify(name, payload, **kwargs):

def fork(*args, **kwargs):
forks.append(True)
assert kwargs.get("destination_config") == destination
return {"session_id": "destination-session", "model_calls": 0}

def construct(*args, **kwargs):
Expand All @@ -85,18 +102,26 @@ def construct(*args, **kwargs):

monkeypatch.setattr(effect_runtime, "effect_runtime_result", verify)
monkeypatch.setattr(codex_checkpoint, "fork_native_checkpoint", fork)
monkeypatch.setattr(codex_checkpoint, "native_session_checkpoint", lambda *a, **k: {"metadata": native})
monkeypatch.setattr(codex_checkpoint, "native_session_checkpoint", lambda *a, **k: {
"metadata": {**native, "config_sha256": hashlib.sha256(config.read_bytes()).hexdigest()}})
monkeypatch.setattr(codex_agent_session, "CodexAgentSession", construct)
if change_goal:
with pytest.raises(ValueError, match="Goal configuration changed"):
codex_checkpoint.admit_native_checkpoint_session(
env, unit, digest, config, sandbox="read-only", timeout=30)
env, unit, digest, source_config, sandbox="read-only", timeout=30,
**({"destination_config": destination} if destination is not None else {}))
assert select_codex_cli_session(runtime, lineage=prepared.lineage, session_scope="agent") == before
else:
result = codex_checkpoint.admit_native_checkpoint_session(
env, unit, digest, config, sandbox="read-only", timeout=30)
env, unit, digest, source_config, sandbox="read-only", timeout=30,
**({"destination_config": destination} if destination is not None else {}))
assert result["session_binding_admitted"] and result["session_id"] == "destination-session"
binding = select_codex_cli_session(runtime, lineage=prepared.lineage, session_scope="agent")
assert binding["checkpoint_artifact_sha256"] == digest
assert binding["checkpoint_artifact_root"] == str(unit)
assert binding["session_profile_digest"] == destination_profile
if replay and model_transition:
assert result["model_transition"]["source_model"] == "synthetic"
assert result["model_transition"]["destination_effort"] == "max"
assert (source_config.read_bytes() != config.read_bytes()) is model_transition
assert len(forks) == (0 if replay else 1)
89 changes: 89 additions & 0 deletions benchmark/tests/test_checkpoint_model_transition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""A new receiver can choose a model without rewriting its captured evidence."""
import hashlib

import pytest

from loopx.hosts.codex_checkpoint import checkpoint_destination_config


@pytest.fixture
def settings(tmp_path):
source, target = tmp_path / "source.toml", tmp_path / "target.toml"
source.write_text('model="source-model"\nmodel_reasoning_effort="high"\n'
'sandbox_mode="read-only"\nmodel_provider="openai"\n'
'[features]\ngoals=false\n')
target.write_text(source.read_text().replace('"source-model"', '"next-model"')
.replace('"high"', '"max"'))
return source, target, hashlib.sha256(source.read_bytes()).hexdigest()


def test_explicit_model_transition_keeps_source_and_all_other_settings(settings):
source, target, digest = settings
before = source.read_bytes()
data, transition = checkpoint_destination_config(source, digest, target)
assert data == target.read_bytes() and source.read_bytes() == before
assert transition == {
"source_config_sha256": digest,
"destination_config_sha256": hashlib.sha256(data).hexdigest(),
"source_model": "source-model", "source_effort": "high",
"destination_model": "next-model", "destination_effort": "max",
}
assert checkpoint_destination_config(source, digest) == (before, None)


@pytest.mark.parametrize("old,new", [
('"read-only"', '"danger-full-access"'), ('"openai"', '"other"'),
('goals=false', 'goals=true'),
])
def test_model_transition_does_not_change_permissions_provider_or_native_goal(settings, old, new):
source, target, digest = settings
target.write_text(target.read_text().replace(old, new))
with pytest.raises(ValueError, match="only model and reasoning effort"):
checkpoint_destination_config(source, digest, target)


def test_model_transition_preserves_source_digest_and_rejects_redirected_target(settings, tmp_path):
source, target, digest = settings
with pytest.raises(ValueError, match="effective configuration differs"):
checkpoint_destination_config(target, digest, source)
link = tmp_path / "target-link.toml"
link.symlink_to(target)
with pytest.raises(ValueError, match="physical absolute path"):
checkpoint_destination_config(source, digest, link)


@pytest.mark.parametrize("replacement", ['model=""', 'model=true', 'model=13'])
def test_model_transition_requires_an_explicit_model(settings, replacement):
source, target, digest = settings
target.write_text(target.read_text().replace('model="next-model"', replacement))
with pytest.raises(ValueError, match="explicit model and reasoning effort"):
checkpoint_destination_config(source, digest, target)


def test_transition_provenance_survives_exact_session_observations_and_qualified_refresh(tmp_path):
from loopx.control_plane.turn_driver.codex_sessions import (
_replace_checkpoint_codex_session, _store_codex_cli_session, load_codex_cli_session,
)
facts = {"source_config_sha256": "a" * 64, "destination_config_sha256": "b" * 64,
"source_profile_digest": "c" * 64, "destination_profile_digest": "d" * 64,
"source_model": "original", "source_effort": "high",
"destination_model": "next", "destination_effort": "max"}
runtime, point = tmp_path / "runtime", tmp_path / "point"
point.mkdir()
binding = dict(lineage={"goal_id": "goal", "agent_id": "receiver"},
session_scope="agent", session_id="fork", session_profile_digest="d" * 64)
_store_codex_cli_session(runtime, **binding, checkpoint_artifact_sha256="e" * 64,
checkpoint_artifact_root=str(point), checkpoint_model_transition=facts)
_store_codex_cli_session(runtime, **binding)
current = load_codex_cli_session(runtime, lineage=binding["lineage"], session_scope="agent")
assert current["checkpoint_model_transition"] == facts
with pytest.raises(ValueError, match="transition cannot change"):
_store_codex_cli_session(runtime, **binding,
checkpoint_model_transition={**facts, "destination_model": "another"})
_replace_checkpoint_codex_session(runtime, lineage=binding["lineage"], expected=current,
session_id="fresh-context", refresh={"checkpoint_sha256": "e" * 64})
refreshed = load_codex_cli_session(runtime, lineage=binding["lineage"], session_scope="agent")
assert refreshed["checkpoint_model_transition"] == facts
_store_codex_cli_session(runtime, **{**binding, "session_id": "unrelated"})
assert "checkpoint_model_transition" not in load_codex_cli_session(
runtime, lineage=binding["lineage"], session_scope="agent")
Loading
Loading