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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1480,3 +1480,54 @@ belongs in this infrastructure handoff, never a replacement solution Todo.
Recovery settles process transport only. Preserved pending semantic replan
attempts still require their original settlement owner; an interrupted replan
is not silently declared complete or cleared to force dispatch.

### Explicitly continue after native account allowance is repaired

A hard native quota failure is still held by default. Repair or replenish the
authorized experiment account through its existing provider and verify its fresh
allowance before requesting another phase. This does not change the desktop
login, waive a Stop, refill evaluator opportunities, or turn hard quota failures
into automatic timed retries.

For a retained complete worker, the same scheduler entrypoint accepts
`--retry-failed-state-sha256 SHA256` naming the exact original scheduler file.
Supply the independently admitted finite remainder in
`LOOPX_PHASE_DEADLINE_EPOCH`, preserving the original phase clock and failed
receipt. The caller must retain the workspace, Goal/SQLite, session, evaluator
history and cooldown; this entrypoint cannot reconstruct missing source data.
For example, after these provider and source checks:

```sh
python -m benchmark.runtime.scheduler \
--python /runtime/bin/python --source /runtime/source \
--state-file /runtime/control/scheduler-state.json --host-timeout 600 \
--retry-failed-state-sha256 EXACT_FAILED_STATE_SHA256
```

The recovery provider requires one returned `quota_exhausted` wake, its original
session/profile binding, terminated native process group and a stable native
`failed` tail with the corresponding structured quota error and no pending tool
effects. Legacy workers can use the same stopped-container proof above, but only
when the exact last returned failure, original session and complete unchanged
filesystem are retained. Unknown errors, missing sessions and incomplete history
remain held. The ordinary completed-checkpoint gate is unchanged; a failed native
Turn is not rewritten or presented as completed.

The shared Turn retry rule decides explicit-retry eligibility. A new append-only
phase records the original failure, digest, native cursor and deadline, retains
the Host attempt count and semantic replan records, and starts through normal
Goal/Stop/session/work admission. Repeating the request selects that same phase,
including any later failure; it cannot clear the failure or extend its deadline.
A later explicit repair must bind that new failure separately. Existing operator
and account authority remain required; a digest is provenance, not permission.

The explicit failed-phase path additionally uses the installed optional native
Codex provider's existing availability policy (`loopx-codex-provider-routing`).
It reads `account/read(refreshToken=false)` and `account/rateLimits/read` in a
fresh disposable inspection home before creating the phase. Missing provider
support, unknown allowance, exhausted allowance or a changed Goal/session keep
the original failure held. Only a bounded availability receipt is retained;
no account response, login values or token is emitted. The ordinary startup
probe and automatic retry path keep their previous behavior. This observation
is not a reservation of future provider capacity; a later failure is retained
and requires another explicit repair.
42 changes: 42 additions & 0 deletions benchmark/edgebench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,48 @@ their existing owners. No worker path, scoring, deadline or capture opportunity
changes. Select this option only for a new run; it does not relocate a running
bank or recover a missed point.

## Preserve a failed worker before teardown

For a new heartbeat run with full native controller capture, add
`--preserve-failed-worker` to preserve the last source after a nonzero or uncertain
solver exit. This is opt-in and requires agent-only submissions (`--eval-interval 0`);
ordinary runs, successful exits and Native Goal completion keep their previous
cleanup behavior. Removing the option disables the policy for later runs; it does
not delete a previously retained worker.

At the exact solver-return boundary, before the native provider stops its work
container, the adapter closes the periodic observer and attempts one bounded joint
capture through the existing Docker writer barrier and native checkpoint owner.
It captures the complete task workspace, declared external research components,
Goal state, scheduler/wake evidence, native session/configuration and native SQLite
participants (including committed WAL data). It never exports the native credential
home or `auth.json`. The native capsule retains its own submission history,
incumbent, opportunity counter, cooldown and original phase. No new submission,
model request, successful settlement or extended clock is created.

The trial's private `terminal-preservation.json` records the original execution,
export outcome/digest, preservation time and confirmed stopped-container state.
The file export explicitly remains **unqualified for restoration**: a failed native
Turn is not a completed conversation checkpoint. Both successful and failed exports
retain the original stopped container until an operator separately qualifies a
receiver or authorizes disposal. Compute is stopped before returning to provider final-archive extraction or
grading drain, and cleanup verifies that stop again;
a stop failure is reported without deleting the source. Missing participants, full
storage or an uncertain writer barrier cannot fall through to force-removal.
The provider still performs its ordinary network/proxy cleanup and grader drain.
Its best-effort final submission archive may be absent because its extraction
requires a running container; latest files remain in the stopped source and, on
successful export, the terminal point. Native graded history/results remain
independent and are not discarded with that optional archive.

Inspect this receipt, the exported manifest and the exact retained container before
planning recovery. Do not restart it, treat a file digest as receiver adoption, or
roll back to an older point to fill a tail gap. Retained containers still occupy
**disk** and may contain their original private credentials; reserve storage and
explicitly dispose of the exact owned container after recovery/disposal approval.
Terminal preservation does not rotate away the last qualified regular checkpoint,
and it does not increase the daily/fresh-replan capture frequency.

## Default research checkpoint retention

**Default change:** new controller-enabled native runs automatically keep only
Expand Down
17 changes: 15 additions & 2 deletions benchmark/edgebench/checkpoint_supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@

class CheckpointSupervisor:
def __init__(self, controller_file: Path, destination: Path, retention_policy: Path | None = None,
*, retention_enabled: bool = True):
*, retention_enabled: bool = True, terminal_components: list[dict[str, str]] | None = None,
terminal_receipt: Path | None = None):
_controller(controller_file) # Fail before native registration.
qualify_export_storage(destination)
if not retention_enabled and retention_policy is not None:
raise ValueError("A retention policy cannot be combined with disabled retention")
self.controller_file, self.destination = controller_file, destination
self.preservation = None
if terminal_components is not None:
from .terminal_capture import FailurePreservation
if terminal_receipt is None or terminal_receipt.exists():
raise ValueError("Terminal preservation requires a fresh private receipt")
self.preservation = FailurePreservation(self, terminal_components, terminal_receipt)
self.token = None
self.stop = threading.Event()
self.thread = None
Expand All @@ -47,6 +54,7 @@ def bind(self, environment):
def start(self, container):
if self.thread is not None or self.token is None:
raise ValueError("Checkpoint supervisor requires one bound worker execution")
self.container = container
self.destination.mkdir(mode=0o700, parents=True, exist_ok=False)
if self.retention_enabled:
from loopx.hosts.checkpoint_retention import _state
Expand All @@ -58,10 +66,15 @@ def start(self, container):
mode=0o600, fsync_directory=True)
self.maintenance = threading.Thread(target=self._maintain, daemon=True)
self.maintenance.start()
self.container = container
self.thread = threading.Thread(target=self._run, args=(container,), daemon=True)
self.thread.start()

def finish_solver(self, execution):
if self.preservation is None:
self.close()
else:
self.preservation.finish(execution)

def close(self):
self.stop.set()
if self.thread is not None:
Expand Down
5 changes: 4 additions & 1 deletion benchmark/edgebench/checkpoint_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ def qualify_export_storage(destination: Path) -> None:


def _request(path: Path) -> dict:
value = json.loads(_physical(path).read_bytes())
return _validate_request(json.loads(_physical(path).read_bytes()))


def _validate_request(value: dict) -> dict:
if (not isinstance(value, dict) or
set(value) not in ({"container_id", "components", "phase_deadline_path"},
{"container_id", "components", "phase_deadline_path", "handoff"}) or
Expand Down
15 changes: 14 additions & 1 deletion benchmark/edgebench/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ def main(argv=None):
help="Host-private native checkpoint controller; requires full fresh-replan handoff")
parser.add_argument("--checkpoint-export-root", type=Path,
help="Existing physical host volume for full worker checkpoints; must preserve Linux filenames")
parser.add_argument("--preserve-failed-worker", action="store_true",
help="Opt-in terminal joint capture and stopped-worker retention after a failed heartbeat execution; requires full controller capture")
parser.add_argument("--checkpoint-request", type=Path,
help="Explicit host JSON declaration staged into a heartbeat worker; omitted disables capture")
parser.add_argument("--restore-checkpoint", type=Path,
Expand Down Expand Up @@ -231,6 +233,10 @@ def main(argv=None):
parser.error("controller file and full controller_handoff must be enabled together with native feedback")
if (args.checkpoint_retention_policy is not None or args.no_checkpoint_retention) and not handoff:
parser.error("Checkpoint retention requires the explicit native controller capture path")
if args.preserve_failed_worker and not handoff:
parser.error("Failed-worker preservation requires full native controller capture")
if args.preserve_failed_worker and args.eval_interval != 0:
parser.error("Failed-worker preservation requires agent-only native submissions")
if args.checkpoint_export_root is not None:
if not handoff:
parser.error("Checkpoint export storage requires full controller capture")
Expand Down Expand Up @@ -405,9 +411,15 @@ def main(argv=None):
from benchmark.edgebench.checkpoint_supervisor import CheckpointSupervisor
destination = (args.checkpoint_export_root / args.run_id / args.task / "checkpoints"
if args.checkpoint_export_root is not None else trial / "checkpoints")
terminal = None
if args.preserve_failed_worker:
from benchmark.edgebench.terminal_capture import terminal_components
terminal = terminal_components(declaration, task.cwd)
checkpoint = CheckpointSupervisor(args.checkpoint_controller_file, destination,
retention_policy=args.checkpoint_retention_policy,
retention_enabled=not args.no_checkpoint_retention)
retention_enabled=not args.no_checkpoint_retention,
terminal_components=terminal,
terminal_receipt=(trial / "terminal-preservation.json") if terminal else None)
backend = RecordingDockerBackend(log_dir=trial / "collected", logger=logger,
oauth_proxy=bool(args.api_proxy_url or api_base_url),
blind_api_endpoint=blind_endpoint, feedback=feedback,
Expand Down Expand Up @@ -475,6 +487,7 @@ def main(argv=None):
**({"checkpoint_request_sha256": hashlib.sha256(checkpoint_request).hexdigest()}
if checkpoint_request is not None else {}),
**({"checkpoint_export_directory": str(checkpoint.destination)} if checkpoint is not None else {}),
**({"preserve_failed_worker": True} if args.preserve_failed_worker else {}),
}
receipt_path = trial / "runtime-receipt.json"
receipt_path.write_text(json.dumps(receipt, indent=2))
Expand Down
33 changes: 29 additions & 4 deletions benchmark/edgebench/stopped_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import hashlib
import io
import json
import math
import os
from pathlib import Path
import re
Expand Down Expand Up @@ -102,24 +103,48 @@ def prepare_stopped_recovery(client, *, container_id: str, declaration: dict,
'files': file_inventory(member), 'directory': member.is_dir()})
state_raw = copied['scheduler_state'].read_bytes()
state = json.loads(state_raw)
if (state.get('host_attempt', {}).get('status') != 'started'
prior = state.get('host_attempt', {})
failed = prior.get('status') == 'failed'
if (prior.get('status') not in {'started', 'failed'}
or json.loads(copied['host_pending'].read_bytes()) != {'pending': True}):
raise ValueError('Stopped legacy recovery requires the original unresolved Host attempt')
wake = copied['wake_log_dir'] / declaration['wake_turn_id'] / 'receipt.json'
raw = wake.read_bytes()
receipt = json.loads(raw)
candidates = [p for p in copied['wake_log_dir'].glob('*/receipt.json')
if p.stat().st_mtime > copied['scheduler_state'].stat().st_mtime]
if failed:
from loopx.control_plane.turn_driver.host_failure import build_host_failure_record, normalize_host_failure_record

failure = normalize_host_failure_record(prior.get('host_failure'))
if (failure['kind'] != 'quota_exhausted' or prior.get('session_id') is None
or failure['attempt'] != prior.get('attempt')
or state.get('last_wake_status') != 'wake_failed'
or type(state.get('last_wake_rc')) is not int or state['last_wake_rc'] != 1
or normalize_host_failure_record(receipt.get('host_failure'))
!= build_host_failure_record(failure['kind'], attempt=1)
or receipt.get('session', {}).get('session_id') != prior['session_id']
or receipt.get('session', {}).get('binding_scope') != 'agent'
or type(receipt.get('return_code')) is not int or receipt['return_code'] != 1
or receipt.get('budget_exhausted')):
raise ValueError('Stopped source lacks its exact returned quota failure')
# A returned failure is committed after its receipt. Do not borrow
# an earlier or later wake, or treat an uncertain started attempt as returned.
all_receipts = list(copied['wake_log_dir'].glob('*/receipt.json'))
newest = max((p.stat().st_mtime for p in all_receipts), default=0)
if candidates or [p for p in all_receipts if p.stat().st_mtime == newest] != [wake]:
raise ValueError('Stopped failure is not the last returned wake')
candidates = [wake]
if candidates != [wake]:
raise ValueError('Stopped source has no unique returned wake after its Host attempt')
if (receipt.get('turn_id') != declaration['wake_turn_id'] or receipt.get('host_invoked') is not True
or receipt.get('budget_exhausted') is not True or receipt.get('mode') != 'heartbeat'
or (not failed and receipt.get('budget_exhausted') is not True) or receipt.get('mode') != 'heartbeat'
or receipt.get('stage') != 'execute' or receipt.get('ok') is not False
or 'host_failure' in receipt):
or (not failed and 'host_failure' in receipt)):
raise ValueError('Stopped source lacks an exact interrupted budget receipt')
start = int(declaration['wake_turn_id'].split('-')[1]) / 1_000_000_000
phase = float(copied['phase_clock'].read_text())
if not start <= phase <= finished:
if not math.isfinite(phase) or not start <= min(phase, finished) or (not failed and phase > finished):
raise ValueError('Stopped source phase did not expire during the original wake')
result = {'container_id': box.id, 'source_state': before, 'declaration': declaration,
'components': components, 'scheduler_state_sha256': hashlib.sha256(state_raw).hexdigest(),
Expand Down
Loading
Loading