# Checkpointing and Rollback

## Overview

CleverAgents supports checkpointing and rollback to allow operators to
snapshot sandbox state during plan execution and restore it later. This
is useful for recovering from mistakes, reverting tool side-effects, and
supporting the decision-correction revert flow.

## Concepts

### Checkpoint

A **Checkpoint** is an immutable record of sandbox state at a point in
time. Each checkpoint stores:

- `checkpoint_id` — Unique ULID identifier
- `plan_id` — The plan that owns this checkpoint
- `sandbox_ref` — A reference to the sandbox state (e.g. git commit hash)
- `decision_id` — Optional decision ULID this checkpoint is aligned to
- `checkpoint_type` — One of `pre_write`, `post_step`, or `manual`
- `resource_id` — Optional resource ULID association
- `filesystem_path` — Relative path within the checkpoint directory
- `size_bytes` — Size of the checkpoint data in bytes
- `created_at` — Timestamp of creation (UTC)
- `metadata` — Audit metadata including reason, source tool, and phase

### Retention Policy

A **CheckpointRetentionPolicy** governs how many checkpoints a plan may
keep. When `auto_prune` is enabled and the number of checkpoints exceeds
`max_checkpoints`, the oldest **interior** checkpoints are automatically
removed when a new one is created. The first (earliest) and most recent
checkpoints are always preserved during pruning.

**Defaults:**

| Field             | Default | Range   |
|-------------------|---------|---------|
| `max_checkpoints` | 50      | 1–100   |
| `auto_prune`      | true    |         |

### Rollback

A **rollback** restores sandbox state to a previously captured checkpoint.
The `RollbackResult` describes what changed:

- `restored_files_count` — Number of files restored
- `changed_paths` — Paths that were modified during rollback
- `from_checkpoint_id` — The checkpoint that was restored

## Guards

The rollback operation enforces two guards:

1. **Plan is applied** — Once a plan reaches the `applied` terminal state,
   rollback is rejected with a `BusinessRuleViolation`.
2. **Sandbox is missing** — If the sandbox has been cleaned up, rollback
   is rejected because there is nothing to restore.

## Automatic Checkpoint Triggers (v3.8.0+)

The execution engine now supports four automatic checkpoint triggers, all
configurable via `core.checkpoints.auto_create_on` (default: all enabled):

| Trigger | When | Component |
|---------|------|-----------|
| `on_tool_write` | Before each write-tool execution | `ToolRunner` |
| `on_tool_write_complete` | After each write-tool execution | `ToolRunner` |
| `on_subplan_spawn` | Before first subplan execution attempt | `SubplanExecutionService` |
| `on_error` | When the Execute phase fails | `PlanExecutor` |

### Configuration

```toml
[core.checkpoints]
auto_create_on = ["on_tool_write", "on_tool_write_complete", "on_subplan_spawn", "on_error"]
```

To disable a specific trigger, remove it from the list. To disable all
automatic checkpoints:

```toml
[core.checkpoints]
auto_create_on = []
```

### Wiring

The `CheckpointService` is injected as an optional parameter into:

- **`ToolRunner`** — `checkpoint_service` and `auto_checkpoint_triggers` parameters
- **`SubplanExecutionService`** — `checkpoint_service`, `auto_checkpoint_triggers`, and `parent_plan_id` parameters
- **`PlanExecutor`** — uses `_is_auto_trigger_active()` helper to check trigger config

When `checkpoint_service` is `None`, all automatic checkpointing is skipped
(backward-compatible no-op).

## CLI Usage

### Create a checkpoint (programmatic)

Checkpoints are created automatically by the execution engine via the four
triggers above, or manually via `CheckpointService.create_checkpoint()`.
The `CheckpointScope` on each tool's capability declaration controls which
tools trigger `on_tool_write` / `on_tool_write_complete` checkpoints.

### Rollback to a checkpoint

```bash
agents plan rollback [--yes|-y] <plan_id> <checkpoint_id>
```

The `--yes` / `-y` flag skips the interactive confirmation prompt.

**JSON output envelope** (when using `--format json`):

```json
{
  "rollback_summary": {
    "plan_id": "...",
    "from_checkpoint_id": "...",
    "restored_files_count": 3
  },
  "changes_reverted": ["path/to/file1.py", "path/to/file2.py"],
  "impact": {
    "files_affected": 3
  },
  "post_rollback_state": {
    "active_checkpoint": "...",
    "plan_id": "..."
  },
  "timing": {
    "elapsed_seconds": 0.042
  },
  "messages": ["Rollback completed successfully."]
}
```

### Error Cases

| Error | Cause |
|-------|-------|
| `Rollback blocked: plan is already applied` | Plan has reached terminal applied state |
| `Rollback blocked: sandbox is missing` | Sandbox was cleaned up before rollback |
| `Not found: checkpoint` | Checkpoint ID does not exist |
| `Validation Error` | Checkpoint does not belong to the specified plan |

## Correction Service Integration

The `CorrectionService` accepts an optional `checkpoint_service` parameter
(typed as `CheckpointService | None`).  This is an **integration point**
for a future milestone: once wired, the correction revert flow will
delegate sandbox restoration to the checkpoint rollback mechanism,
enabling reuse of the same rollback logic for both explicit CLI rollback
and decision-correction reverts.  The delegation is **not yet active** in
the current implementation; `execute_revert()` performs BFS impact
analysis and marks decisions as reverted but does not invoke
`checkpoint_service.rollback_to_checkpoint()`.

## Database Schema

The `checkpoint_metadata` table stores checkpoint metadata:

| Column            | Type        | Description                         |
|-------------------|-------------|-------------------------------------|
| `checkpoint_id`   | STRING(26) PK | ULID primary key                  |
| `plan_id`         | STRING(26) FK | References `v3_plans.plan_id`     |
| `decision_id`     | STRING(26)  | Optional decision alignment         |
| `checkpoint_type` | TEXT        | `pre_write`, `post_step`, `manual`  |
| `resource_id`     | STRING(26)  | Optional resource association        |
| `sandbox_ref`     | TEXT        | Git commit hash or patch reference  |
| `filesystem_path` | TEXT        | Relative path within checkpoint dir |
| `size_bytes`      | INTEGER     | Size of checkpoint data in bytes    |
| `created_at`      | STRING(30)  | ISO-8601 timestamp                  |
| `metadata_json`   | TEXT        | JSON: reason, source_tool, phase    |

Indexes: `idx_checkpoints_plan`, `ix_checkpoint_metadata_created_at`.

## Dependency Injection

The `CheckpointService` is registered in the DI container
(`container.py`) and backed by a `CheckpointRepository` that uses the
session-factory pattern (ADR-007). The CLI `rollback` command obtains
the service from the container via `get_container().checkpoint_service()`.

The `UnitOfWorkContext` also exposes a `checkpoints` property for
cross-repository atomicity within a single transaction.

## Metadata for Auditability

Each checkpoint stores structured metadata to support audit trails:

- **reason** — Why the checkpoint was created (e.g. "pre-execution")
- **source_tool** — Which tool triggered the checkpoint (e.g. "lint-check")
- **phase** — Which plan phase was active (e.g. "execute")
- **extra** — Arbitrary key-value pairs for extension
