Files
cleveragents-core/docs/adr/ADR-035-decision-tree-rollback-and-replay.md
HAL9000 f808abff86 chore(ci): fix pre-commit hook failures
Fix JSON syntax errors in .devcontainer/devcontainer.json (removed
invalid JS-style // comments) and .devcontainer/opencode.json (removed
90+ trailing commas). Apply auto-fixes for end-of-file and trailing
whitespace issues across 100+ files. Fix SIM105 ruff violations in
benchmarks/core_circuit_breaker_bench.py (use contextlib.suppress).

Note: The security fix from issue #7478 (validate_path startswith bypass)
was already delivered to master in commit e18ac5f2. This PR as currently
structured is non-atomic (35 commits across 10+ issues) and needs
significant restructure before merge. This commit only addresses the
CI/pre-commit failures.

ISSUES CLOSED: #7478
2026-06-14 09:51:14 -04:00

25 KiB

adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
adr_number title status_history tier authors superseded_by related_adrs acceptance
35 Decision Tree Rollback and Replay
2026-02-20
Proposed
Jeffrey Phillips Freeman
2026-02-20
Accepted
Jeffrey Phillips Freeman
2
Jeffrey Phillips Freeman
null
number title relationship
6 Plan Lifecycle Phase reversion interacts with decision correction; corrections may trigger phase changes
number title relationship
7 Decision Tree and Correction Defines the correction model that this ADR provides rollback mechanics for
number title relationship
15 Sandbox and Checkpoint Checkpoint infrastructure that decision-aligned checkpointing builds upon
number title relationship
33 Decision Recording Protocol The `record_decision` tool triggers checkpoint group creation during Execute
number title relationship
34 Decision Tree Versioning and History Superseded branch mechanics used during correction cascading
votes_for votes_against abstentions
voter comment
Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com> Decision-aligned checkpointing with coordinated cross-resource rollback enables fine-grained Execute-phase correction

Context

ADR-007 defines a high-level 6-step correction flow for reverting decisions, and ADR-034 defines how superseded branches are tracked. However, neither ADR details the mechanics of actually rolling back to an arbitrary decision point — especially during the Execute phase where resources have been modified. The correction model must handle two fundamentally different scenarios:

  1. Mid-Strategize correction: Strategize is read-only, so there are no resource modifications to roll back. Only the actor's reasoning state needs to be restored to the decision point.

  2. Mid-Execute correction: Execute modifies resources via tool calls. Rolling back to an arbitrary Execute-phase decision requires restoring both the resource state (sandbox contents) and the actor's reasoning state to the exact moment that decision was made.

The second scenario is significantly harder. Resource state changes continuously throughout Execute as tools are invoked. Without a mechanism to capture resource state at each decision point, the system cannot roll back to arbitrary decisions during Execute — it can only revert the entire Execute phase back to Strategize, which is expensive and discards all valid work.

Decision Drivers

  • Mid-Execute corrections require restoring both resource state (sandbox contents) and actor reasoning state to the exact decision point
  • Mid-Strategize corrections are read-only and need only reasoning state restoration, not resource rollback
  • Without decision-aligned checkpoints, the only Execute-phase recovery option is reverting the entire phase back to Strategize, discarding all valid work
  • Rollback must coordinate across multiple sandbox strategies (git worktree, filesystem copy, database savepoints) within a single correction
  • Non-rollbackable resources and irreversible side effects (e.g., external API calls) must be explicitly tracked and warned about rather than silently ignored
  • Checkpoint overhead must fit within the plan's checkpoint budget, leveraging the fact that decisions are far less frequent than tool calls

Decision

Rollback operates on two independent dimensions: resource rollback (undoing sandbox changes via checkpoint restoration) and reasoning rollback (restoring actor state from the decision's context_snapshot.actor_state_ref). Execute-phase decisions automatically trigger coordinated checkpoint groups across all affected sandbox resources, creating a 1:1 mapping between decisions and resource states that enables fine-grained rollback to any Execute-phase decision point.

Design

Two Rollback Dimensions

Dimension What It Restores Mechanism Applicable Phase
Resource rollback Sandbox contents (files, database state, filesystem) Checkpoint restoration via sandbox-strategy-specific handlers Execute only
Reasoning rollback Actor's conversation history, intermediate reasoning, graph state LangGraph checkpoint restoration from actor_state_ref Strategize and Execute

For Strategize-phase corrections, only reasoning rollback is needed (Strategize is read-only — there are no resource modifications to undo). For Execute-phase corrections, both dimensions are required.

Mid-Strategize Correction Flow

When agents plan correct <decision_id> --mode=revert targets a Strategize-phase decision:

  1. Validate: Confirm the target decision belongs to the current plan and has superseded_by IS NULL.
  2. Load context: Read the target decision's context_snapshot, specifically the actor_state_ref (LangGraph checkpoint).
  3. Restore actor state: Restore the strategy actor's LangGraph state from actor_state_ref. This places the actor at the exact reasoning state it was in when the target decision was made — same conversation history, same intermediate reasoning, same loaded context.
  4. Supersede affected subtree: Mark the target decision and all its descendants as superseded (see ADR-034 for cascade mechanics). The decisions above the target in the tree remain active and unchanged.
  5. Inject guidance: Create a user_intervention decision containing the user's --guidance text. This decision becomes part of the actor's context, informing it of the correction.
  6. Resume strategy actor: The strategy actor continues reasoning from the restored state, now informed by the user's guidance. It creates new decisions (via record_decision per ADR-033) to replace the superseded ones.
  7. Record correction: Create a correction_attempts record linking the original decision to the new replacement decision.

No resource rollback occurs — there is nothing to roll back during Strategize.

Execute-Phase Decision-Aligned Checkpointing

The key mechanism enabling fine-grained Execute-phase rollback is decision-aligned checkpointing: every record_decision call during the Execute phase automatically triggers creation of a coordinated checkpoint group.

Checkpoint Group Creation

When record_decision is called during Execute:

  1. Enumerate modified resources: Identify all sandbox resources that have been written to during this plan's execution up to this point.

  2. Create per-resource checkpoints: For each modified resource, create a checkpoint using the resource's sandbox-strategy-specific mechanism:

    Sandbox Strategy Checkpoint Mechanism Stored As
    git_worktree Git commit in the worktree branch Commit SHA
    filesystem_copy Snapshot of modified files Compressed archive
    overlay Overlay layer state marker Layer reference
    transaction_rollback Database SAVEPOINT Savepoint name
    none Marker only (non-rollbackable) Marker record
  3. Link to decision: Each checkpoint is stored in checkpoint_metadata with:

    • decision_id = the decision being recorded (this is the group key)
    • resource_id = the specific resource this checkpoint covers
    • checkpoint_type = 'decision_point'
    • For non-rollbackable resources: checkpoint_type = 'non_rollbackable'
  4. Unmodified resources: Resources that have not been written to at the time of the decision have no checkpoint row. This is correct — they are still in their pre-execution baseline state and do not need rollback.

All checkpoints sharing the same decision_id form a checkpoint group — the complete resource state at that decision point.

Why This Works

Since actors record decisions via the record_decision tool call (ADR-033), and this tool is treated as a write operation, checkpoint creation integrates naturally with the existing checkpoint system (ADR-015). The sequence during Execute is:

Actor reasons about an implementation approach
  │
  ▼
Actor calls record_decision(type=implementation_choice, ...)
  │
  ├── System captures context_snapshot (hot context + LangGraph state)
  ├── System creates checkpoint group (one per modified resource)
  ├── System persists the decision record
  │
  ▼
Actor calls edit_file(...), write_file(...), etc. to implement
  │
  ├── Regular pre-write checkpoints per existing ADR-015 rules
  │
  ▼
Something unexpected happens (error, validation failure, etc.)
  │
  ▼
Actor calls record_decision(type=error_recovery, ...)
  │
  ├── System creates another checkpoint group (captures state including tool effects)
  ├── System persists the decision record
  │
  ▼
Actor implements the recovery...

Rolling back to the implementation_choice decision restores resource state to the checkpoint taken before any of the implementation tool calls. Rolling back to the error_recovery decision restores state to the checkpoint taken after the failed tool calls but before the recovery implementation.

Mid-Execute Correction Flow

When agents plan correct <decision_id> --mode=revert targets an Execute-phase decision:

  1. Validate: Confirm the target decision belongs to the current plan, has superseded_by IS NULL, and was created during the Execute phase.

  2. Load checkpoint group: Query checkpoint_metadata WHERE decision_id = <target> to retrieve all per-resource checkpoints.

  3. Classify checkpoints:

    • Rollbackable: checkpoints with checkpoint_type = 'decision_point' (resources with sandboxes that support rollback)
    • Non-rollbackable: checkpoints with checkpoint_type = 'non_rollbackable' (resources with sandbox.strategy: none)
    • Unaffected: resources with no checkpoint for this decision (not yet modified at decision time)
  4. Warn about non-rollbackable resources: If any non-rollbackable checkpoints exist, present a warning to the user:

    WARNING: The following resources cannot be rolled back (sandbox strategy: none):
      - local/external-api
      - local/notification-service
    
    Resource changes since this decision will persist.
    Proceed with rollback of other resources? [y/N]
    

    The user must confirm to proceed. If they abort, the correction is cancelled.

  5. Check for irreversible side effects: Examine all decisions downstream of the target for tool_invocation decisions that used tools with irreversible side_effects declarations. If any exist, warn:

    WARNING: Rolling back past this decision undoes work that triggered
    irreversible external effects:
      - local/deploy-to-staging (1 invocation) — external deployment
      - local/send-notification (2 invocations) — emails sent
    
    Sandbox resources can be rolled back, but external effects cannot be undone.
    Proceed? [y/N]
    
  6. Resource rollback: For each rollbackable checkpoint, dispatch to the strategy-specific handler:

    Strategy Rollback Operation
    git_worktree git reset --hard <commit_sha> in the worktree
    filesystem_copy Restore from compressed archive at filesystem_path
    overlay Discard overlay writes since the layer reference
    transaction_rollback ROLLBACK TO SAVEPOINT <savepoint_name>

    If any individual resource rollback fails, the correction is marked as 'failed' and the plan enters the errored state. The system logs which resources were successfully rolled back and which failed.

  7. Reasoning rollback: Restore the execution actor's LangGraph state from context_snapshot.actor_state_ref.

  8. Supersede affected subtree: Mark the target decision and all descendants as superseded (ADR-034 cascade mechanics).

  9. Inject guidance: Create a user_intervention decision with the user's --guidance text.

  10. Resume execution actor: The actor continues from the restored state with the correction guidance.

  11. Record correction: Create a correction_attempts record with mode 'revert' linking the original and new decisions.

Three Rollback Tiers

The system's rollback capability depends on configuration:

Tier Prerequisites Capability When to Use
Full decision-level Checkpointing enabled (sandbox.checkpoint.enabled = true) AND sandboxable resources Roll back to any individual Execute-phase decision Default for most plans; best correction granularity
Phase-level Checkpointing disabled OR resources lack sandbox support Revert entire Execute phase → Strategize; re-execute from scratch Low-stakes plans where checkpoint overhead is unwarranted
No rollback sandbox.strategy: none AND checkpointing disabled Execute-phase decisions are one-way; only --mode=append corrections available External API-only plans, non-sandboxable environments

The system automatically detects the available tier when a correction is requested and informs the user if the requested correction exceeds the available capability:

$ agents plan correct 01HXM... --mode=revert --guidance "Use async instead"

ERROR: Decision 01HXM... was made during Execute, but checkpointing is disabled
       for this plan. Fine-grained rollback is not available.

Available options:
  - Revert to Strategize: agents plan correct <strategize_decision_id> --mode=revert
  - Append correction: agents plan correct 01HXM... --mode=append --guidance "..."
  - Re-run with checkpointing: enable sandbox.checkpoint.enabled and re-execute

Affected Subtree Computation

When correcting a decision, the system must identify all downstream decisions and plans that are affected (the "affected subtree"). This computation uses the influence DAG (not the structural tree):

def compute_affected_subtree(target_decision_id: str) -> tuple[set[str], set[str]]:
    """Returns (affected_decision_ids, affected_plan_ids)."""
    affected_decisions = {target_decision_id}
    affected_plans = set()
    queue = [target_decision_id]

    while queue:
        current = queue.pop(0)

        # Follow structural tree children
        children = db.query(
            "SELECT decision_id FROM decisions "
            "WHERE parent_decision_id = :current AND superseded_by IS NULL",
            current=current
        )

        # Follow influence DAG dependents
        dependents = db.query(
            "SELECT downstream_ref FROM decision_dependencies "
            "WHERE upstream_decision_id = :current AND dependency_type = 'decision'",
            current=current
        )

        for decision_id in children | dependents:
            if decision_id not in affected_decisions:
                affected_decisions.add(decision_id)
                queue.append(decision_id)

        # Collect affected child plans
        child_plans = db.query(
            "SELECT downstream_ref FROM decision_dependencies "
            "WHERE upstream_decision_id = :current AND dependency_type = 'plan'",
            current=current
        )
        affected_plans.update(child_plans)

    return affected_decisions, affected_plans

Cross-Plan Correction Cascading

When the affected subtree includes child plans (via subplan_spawn or subplan_parallel_spawn decisions):

  1. Child plan not yet started: Cancel the child plan. It will be re-spawned if the corrected parent decision still calls for it.
  2. Child plan in progress (Strategize or Execute): Cancel the child plan. Roll back its sandbox. It will be re-spawned with potentially different parameters after the parent's correction replays.
  3. Child plan completed but not applied: Cancel the child plan. Roll back its sandbox. Same as above.
  4. Child plan already applied: Correction is rejected. Once a child plan's changes have been committed to real resources, the parent cannot unilaterally roll them back. The user must correct the child plan independently or use --mode=append on the parent.

Phase Boundary Interaction

agents plan correct is orthogonal to phase reversion (Execute → Strategize). They are different mechanisms triggered by different actors:

Mechanism Trigger Direction Use Case
Phase reversion Execution actor (constraints too tight) Execute → Strategize Actor-initiated; strategy needs adjustment
Decision correction User (plan correct) Any decision → re-run from that point User-initiated; decision was wrong

These can interact:

  • Correcting a Strategize-phase decision while the plan is in Execute: The plan reverts to Strategize at the corrected decision point. All Execute-phase work is rolled back via checkpoints. The strategy actor resumes from the corrected decision, produces a new strategy, and Execute restarts with the new strategy.

  • Correcting an Execute-phase decision while the plan is in Execute: The plan remains in Execute. Resources are rolled back to the decision's checkpoint. The execution actor resumes from that point with correction guidance. No phase change occurs.

  • Correcting any decision while the plan is in Apply: If the plan is in the constrained or errored Apply state, correction reverts to the appropriate phase. If the plan is already applied (terminal success), correction is rejected — applied changes cannot be reverted.

Checkpoint Budget Considerations

Decision-aligned checkpoints consume from the plan's checkpoint budget (sandbox.checkpoint.max-per-plan, default: 50). Since Execute-phase decisions are relatively rare compared to tool calls (typically 5-10 decisions vs. 30-50 tool calls), decision checkpoints fit within the default budget.

When the budget is exceeded, the existing pruning algorithm applies (keep first + most recent, prune oldest intermediate). Decision-linked checkpoints should be given higher retention priority than regular pre-write checkpoints during pruning, because losing a decision checkpoint eliminates the ability to roll back to that specific decision.

The pruning priority order is:

  1. First checkpoint (always retained)
  2. Most recent checkpoint (always retained)
  3. Decision-linked checkpoints (checkpoint_type = 'decision_point')
  4. Regular pre-write checkpoints (checkpoint_type = 'pre_write')

Long-Running Database Transaction Constraint

The transaction_rollback sandbox strategy uses SAVEPOINTs within a single database transaction spanning the entire Execute phase. Decision-aligned checkpoints add SAVEPOINTs at each decision point, which are lightweight within an existing transaction. However, the underlying long-running transaction itself may be problematic for plans with extended execution times:

  • Database transaction timeouts may be exceeded.
  • Lock contention prevents other connections from accessing modified rows.
  • Undo log / WAL growth accumulates for the duration of the transaction.

Recommended mitigations (the choice depends on the plan's expected duration and database characteristics):

  1. Decompose into child plans: Each child plan gets its own shorter transaction. The parent plan coordinates sequential or parallel execution. This aligns naturally with the hierarchical decomposition model.
  2. Use none strategy with append-mode corrections: Accept that database changes during Execute are not individually rollbackable. Use --mode=append corrections to fix issues by adding compensating changes.
  3. Implement a custom resource type with snapshot-based strategy: A custom database-snapshot resource type could use point-in-time snapshots (e.g., pg_dump, database cloning, logical replication slots) instead of long-running transactions, trading storage for transaction duration.

Decision-aligned checkpoints (SAVEPOINTs) do not significantly worsen the long-running transaction problem — they are lightweight additions within an already-open transaction. The problem is inherent to the transaction_rollback sandbox strategy itself.

Constraints

  • Execute-phase record_decision calls must trigger checkpoint group creation. This is mandatory, not optional. The system must not persist an Execute-phase decision without a corresponding checkpoint group.
  • Checkpoint groups must be atomic: either all per-resource checkpoints in a group are created, or none are. If checkpoint creation fails for any resource, the record_decision call fails and the decision is not persisted.
  • Resource rollback during correction must be all-or-nothing for rollbackable resources. If any rollbackable resource fails to roll back, the correction fails and the plan enters errored state.
  • Correcting a decision whose affected subtree includes an applied child plan is rejected. The user must correct the child plan independently.
  • Checkpoint pruning must prioritize decision-linked checkpoints over regular pre-write checkpoints.
  • The context_snapshot.actor_state_ref must be a valid, restorable LangGraph checkpoint at the time of the record_decision call.

Consequences

Positive

  • Fine-grained Execute-phase rollback enables correcting individual decisions without re-executing the entire plan from scratch.
  • The three-tier model gracefully degrades: full rollback when possible, phase-level when checkpointing is unavailable, append-only when nothing is sandboxed.
  • Coordinated checkpoint groups ensure consistent multi-resource rollback.
  • The mechanism reuses existing checkpoint infrastructure (ADR-015) — no new storage or rollback machinery is needed.
  • Non-rollbackable resources are explicitly tracked and warned about, preventing silent data loss.

Negative

  • Checkpoint group creation at every Execute-phase decision adds latency and storage overhead.
  • Multi-resource rollback coordination adds complexity to the correction engine.
  • The three-tier model means correction capability varies by configuration, requiring clear user communication about what is possible.

Risks

  • LangGraph checkpoint restoration may not perfectly reproduce the actor's state if the LangGraph version or model has changed between the original decision and the correction attempt.
  • Checkpoint group creation failure (e.g., disk full, database connection lost) prevents the decision from being recorded, potentially blocking plan execution.
  • Irreversible side effects from tool invocations cannot be compensated by resource rollback alone. The system can only warn, not prevent.
  • Coordinated multi-strategy rollback (e.g., git reset + SAVEPOINT ROLLBACK in the same correction) has no cross-resource atomicity guarantee — one may succeed while the other fails.

Alternatives Considered

Nearest-checkpoint rollback with replay — Instead of checkpointing at every decision, find the nearest existing checkpoint before the target decision, roll back to it, and replay tool calls from the execution log up to the decision point. This reduces checkpoint count but introduces complexity (tool replay may not be deterministic) and latency (replaying many tool calls). Rejected because decision-aligned checkpointing is simpler and the checkpoint budget is adequate.

Phase-level rollback only (no decision-level Execute rollback) — Always revert to Strategize when an Execute-phase decision is wrong. Simpler but prohibitively expensive for large plans where only a late Execute-phase decision needs correction. The decision-aligned approach preserves all valid work up to the corrected decision.

Compensating transactions for cross-resource atomicity — Implement a two-phase commit protocol across sandbox strategies to guarantee atomic multi-resource rollback. Rejected as over-engineered — the sandbox strategies are heterogeneous (git, filesystem, database) and a distributed transaction coordinator across them would be extremely complex for marginal benefit.

Compliance

  • Decision-checkpoint linkage tests: Tests verify that every Execute-phase record_decision call creates a checkpoint group with one entry per modified resource.
  • Checkpoint group atomicity tests: Tests verify that checkpoint group creation is all-or-nothing (if one resource's checkpoint fails, no checkpoints are created and the decision is not recorded).
  • Mid-Strategize correction tests: BDD scenarios verify the full mid-Strategize correction flow: actor state restoration, subtree superseding, guidance injection, and strategy actor resumption.
  • Mid-Execute correction tests: BDD scenarios verify the full mid-Execute correction flow: checkpoint group loading, resource rollback, actor state restoration, subtree superseding, and execution actor resumption.
  • Cross-sandbox rollback tests: Integration tests verify coordinated rollback across multiple resources with different sandbox strategies (e.g., git_worktree + transaction_rollback).
  • Non-rollbackable warning tests: Tests verify that corrections targeting decisions with non-rollbackable resources produce appropriate warnings.
  • Irreversible side effect warning tests: Tests verify that corrections passing through tool invocations with irreversible side effects produce appropriate warnings.
  • Tier detection tests: Tests verify that the system correctly identifies the available rollback tier and produces appropriate error messages when the requested correction exceeds the available tier.
  • Cross-plan cascade tests: Tests verify that superseding a subplan_spawn decision correctly handles child plans in each possible state (not started, in progress, completed, applied).
  • Checkpoint pruning priority tests: Tests verify that decision-linked checkpoints are retained over regular pre-write checkpoints during pruning.