Progressive Rollout Gating for Self-Modifications¶
This document specifies a promotion model for self-modification candidates in EVOSEAL's evolution pipeline. Instead of immediately adopting every change that passes regression tests, candidates progress through stability stages before permanent adoption.
Implementation status. This is a design document. The gating mechanism described here is not yet implemented — the current pipeline adopts or rejects changes in a single step.
1. Motivation¶
The evolution pipeline currently treats should_continue as a binary gate: a
candidate either passes validation and is adopted, or it fails and is discarded.
This has two problems:
-
Flaky improvements. A change that passes regression tests once may introduce subtle instability that only surfaces across multiple cycles. Single-shot validation cannot distinguish a genuinely better variant from one that happened to pass on a favorable test run.
-
No graduated trust. All adopted changes are treated identically — a formatting tweak and a core-algorithm rewrite both go live immediately. Riskier changes deserve more scrutiny before they are considered permanent.
Progressive rollout gating addresses both by requiring candidates to demonstrate stability over time, not just at adoption time.
2. Stage Model¶
Every self-modification candidate occupies one of three stages:
| Stage | Meaning | Promotion condition |
|---|---|---|
| candidate | Under initial evaluation | Passes regression tests and the _validate_improvement gate |
| beta | Active but not permanent | Survives N additional evolution cycles without regression |
| stable | Permanently adopted | Promoted from beta after N clean cycles |
Stage transitions¶
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ candidate │───▶│ beta │───▶│ stable │
└──────────┘ └─────┬────┘ └──────────────┘
│
│ regression
▼
┌──────────┐
│ rejected │
└──────────┘
- candidate → beta: the candidate passes the initial validation gate
(
_validate_improvement), exactly as today. At this point the change is applied to the working tree but tracked as beta. - beta → stable: the candidate survives N consecutive evolution cycles without any regression being detected. N is configurable (default: 3).
- beta → rejected: a regression is detected during a subsequent cycle. The change is rolled back and recorded as rejected.
Note. Rejection before the candidate stage (i.e. when
_validate_improvementfails) is handled by the existing pipeline — noRolloutCandidaterecord is created, so it does not appear in the state machine above. The rollout state machine only tracks candidates that passed the initial gate.
What "regression" means at each stage¶
| Stage | Regression signal |
|---|---|
| beta | Any subsequent cycle's validation detects a metric drop against the beta candidate's baseline snapshot |
| stable | Terminal stage — no longer monitored for regression. Defects discovered after promotion are handled through the normal bug-fix pipeline, not rollout rollback. |
3. Data Model¶
Each tracked candidate needs:
@dataclass
class RolloutCandidate:
candidate_id: str # e.g. checkpoint version id
stage: str # "candidate" | "beta" | "stable" | "rejected"
created_at: datetime # when the candidate was first created
promoted_to_beta_at: datetime | None
promoted_to_stable_at: datetime | None
clean_cycles: int # count of consecutive cycles without regression
baseline_metrics: dict # metrics snapshot at creation time
checkpoint_path: str # for rollback
rejection_reason: str | None # regression detail when stage is "rejected"
rejected_at: datetime | None # when the regression was detected
Every stage transition is timestamped (created_at, promoted_to_beta_at,
promoted_to_stable_at, rejected_at) so a candidate's full history can be
reconstructed from the registry alone — without this, a rejected record says
why it was rejected but not when, which makes post-hoc debugging of a
regression impossible to correlate with the cycle that caused it.
This can be stored as a JSON file alongside the existing version registry
(e.g. rollout_registry.json) or as an extension to ModelVersionManager's
registry.
4. Integration Points¶
4.1 Evolution Pipeline (_validate_improvement)¶
The existing _validate_improvement gate remains the first filter. When it
accepts a candidate, instead of immediately adopting it permanently, the
pipeline should:
- Record the candidate as
stage="candidate"with a baseline metrics snapshot. - Apply the change (as it does today).
- Promote to
betaimmediately (the candidate has already passed the validation gate, so no dwell period at the candidate stage is needed — the stage exists to register the checkpoint and baseline metrics before the beta observation window begins).
4.2 Continuous Evolution Service¶
The continuous evolution loop (_run_evolution_cycle) should, at the start of
each cycle:
- Check all active beta candidates.
- Run validation against each beta candidate's baseline metrics.
- If clean: increment
clean_cycles. Ifclean_cycles >= N: promote tostable. - If regression: mark
rejected(record the regression detail inrejection_reason). Ifauto_rollback_on_regressionistrue(§5/§6), roll back to the candidate's checkpoint; otherwise log the regression and leave the working tree unchanged for manual intervention.
Baseline scope.
baseline_metricsis captured once at candidate creation time (§4.1) and remains fixed for the entire beta observation window — it is not re-snapshotted each cycle. This means a candidate can accumulate N individually-clean cycles against the original baseline while the surrounding codebase drifts. The tradeoff is deliberate: a fixed baseline makes promotion decisions deterministic and independent of cycle ordering. The risk of cumulative drift is mitigated by the short default window (N=3) and by the fact that each cycle already runs the full regression suite. A rolling baseline would catch slow drift but would also make promotion non-deterministic (the same candidate could promote or regress depending on what else landed in the interim).Concurrent beta candidates. More than one candidate can occupy
betaat the same time, and they are not isolated from each other: all candidates share a single working tree, and step 2 validates against the pipeline's shared metrics history rather than re-running each candidate's change in isolation. Two consequences follow, both deliberate for this first iteration:
- Attribution is approximate. When several candidates are in
betaand a regression appears, every active beta candidate sees the same failing comparison, so all of them are rejected — the design does not attempt to identify which change actually caused it. This is fail-safe (a real regression is never promoted) but not precise (innocent candidates are rejected alongside the guilty one).- Rollback is per-candidate, not layered. Each candidate records its own
checkpoint_pathcaptured before its change was applied. Restoring one candidate's checkpoint therefore also discards any changes layered on top of it by later candidates. With simultaneous betas this is a blunt instrument.The precise alternatives — serializing beta candidates one at a time, or a layered/patch-based checkpoint model that can revert a single change out of a stack — are deferred (§8). Deployments that need exact attribution should set
beta_cycles_required: 1or otherwise keep at most one candidate in beta.
4.3 Bidirectional Manager¶
BidirectionalEvolutionManager.run_loop_cycle() should be aware of rollout
state. A model deployed from a beta candidate should be flagged as
pre-stable. The prefer_stable_for_generation flag (§5) controls whether the
generation surface prefers stable models for production workloads while
using beta models for experimentation — this section's "pre-stable" flag
is the implementation signal that flag consumes.
5. Configuration¶
Add to configs/safety.yaml (which already holds regression thresholds and checkpoint policy):
rollout_gating:
enabled: true
beta_cycles_required: 3 # N: cycles a beta must survive
auto_rollback_on_regression: true
prefer_stable_for_generation: true # generation uses stable model when available
When enabled is false, gating is bypassed end to end and the pipeline
behaves exactly as it does today: §4.1 registers no RolloutCandidate when
_validate_improvement accepts a change (it is adopted permanently in one
step), and §4.2's per-cycle beta check returns immediately without validating
or promoting anything. Candidates already recorded in the registry from a
previous run are left untouched — they are neither promoted nor rejected while
gating is off, and resume being checked if it is re-enabled.
6. Rollback¶
When a regression is detected at any stage:
- Mark the candidate as
rejected, recording the regression details inrejection_reasonand the detection time inrejected_at(§3). - If
auto_rollback_on_regressionistrue(§5), restore the checkpoint captured at candidate creation time. If the candidate wasbeta, the working tree reverts to the pre-candidate state — the N clean cycles it accumulated are discarded. - If
auto_rollback_on_regressionisfalse, log the regression and leave the working tree unchanged for manual intervention.
This leverages the existing CheckpointManager (fixed in PR #74 for path
traversal) and is consistent with the current rollback-on-failure pattern.
Note that with several candidates in beta simultaneously, restoring one
candidate's checkpoint also discards later candidates layered on top of it —
see the concurrency note in §4.2.
7. Relationship to Existing Patterns¶
This design is analogous to release channels in software distribution:
| Rollout stage | Software release analogue |
|---|---|
| candidate | Nightly / dev build |
| beta | Beta channel — active but not default |
| stable | Stable release — promoted for general use |
OpenClaw uses npm dist-tags (stable, beta, dev) to serve different
versions to different users. EVOSEAL's rollout gating applies the same
graduated-trust principle to autonomous self-modifications rather than
user-facing releases.
8. Future Extensions¶
- Graduated rollout: instead of all-or-nothing promotion, weight beta
candidates by
clean_cycles / Nand use that weight in selection. - Multi-metric gates: require stability across multiple dimensions (correctness, runtime, token cost) rather than a single pass/fail.
- Per-candidate isolation: serialize beta candidates, or adopt a layered/patch-based checkpoint model, so a regression can be attributed to and reverted from a single candidate without rejecting or discarding the others (§4.2).
- Human override: allow manual promotion/demotion via the dashboard (ties into the "Human-in-the-loop feedback interface" P3 item).