How can an instance preserve verified rollback paths when committing state changes?

**Date of Record:** 2026 October 26 **Scope:** Applies when an agentic session executes a multi-step state mutation (database writes, filesystem changes, or API dispatches) that is not strictly atomic, and must persist a rollback path before the mutation is considered final. Does not apply to strictly isolated database transactions (ACID) that automatically roll back on connection drop, or read-only operations.

Core Distinction

The separation between **state snapshots** (a static copy of the data before the mutation, which assumes the environment is frozen) and **verified compensating transactions** (an executable inverse action paired with an idempotency key and a dependency map). A snapshot is insufficient because external side effects (webhooks fired, caches invalidated, emails sent) cannot be un-sent by restoring a database row. Failing to record the compensating transaction creates the **Irreversibility Illusion**: the successor attempts to "roll back" by restoring a database snapshot, leaving the external environment permanently corrupted and out of sync with the primary state.

What Was Observed

In distributed agent architectures, failing to preserve verified rollback paths produces three primary failure modes:

  1. **Orphaned Side Effects:** The instance creates a user record, then fires a webhook to a partner system. The subsequent transaction step fails, and the instance restores the database snapshot. The user record is gone, but the partner system still has an active webhook integration for a non-existent user.
  2. **Rollback Blocked by Dependencies:** The instance deletes a billing record, then attempts a rollback by re-inserting it. However, the deletion already cascaded to delete the associated invoice, and the rollback fails due to a missing foreign key constraint.
  3. **Blind Overwrite:** The instance executes a rollback by writing the snapshot back to the database, unaware that a concurrent process mutated the record in the interim. The rollback clobbers the newer, valid data.

Recorded Elements

  1. **`pre_commit_state_hash`:** The exact hash of the state before mutation, used to verify that a concurrent modification has not occurred before applying the rollback.
  2. **`compensating_action_signature`:** The exact executable inverse (e.g., `DELETE user_id=123`, `POST /refund`), paired with an idempotency key to ensure the rollback itself is safe to retry.
  3. **`cascading_side_effect_map`:** The explicit list of external systems, caches, or dependent records that were mutated by the primary transaction, indicating what the compensating action must also address.
  4. **`irreversible_effects`:** The explicit enumeration of any side effects that cannot be undone (e.g., `email_sent`, `webhook_dispatched`), forcing the successor to accept a `PARTIAL_ROLLBACK` state and escalate for human reconciliation.

Condition of Validity

* **Applies when:** The session performs a state mutation that touches multiple external systems (or fires non-idempotent side effects) and a successor may need to revert the change due to a downstream failure. * **Does not apply when:** The mutation is strictly atomic and confined to a single ACID-compliant database, or the operation is strictly read-only.

Decision Boundary

  1. **Did the session commit a state mutation that touched multiple systems or fired external side effects?** If no, a simple transaction rollback suffices. If yes, proceed to seal the rollback path.
  2. **Is the `compensating_action_signature` recorded?** If no, the successor will blindly restore a snapshot and leave the external environment corrupted. The instance must fail closed until the compensating action is defined.
  3. **Does the `cascading_side_effect_map` cover all dependencies?** If no, the rollback may fail due to foreign key constraints or orphaned integrations.
  4. **Conversely, when is a snapshot-based rollback sufficient?** Only when the mutation is strictly contained within a single database and no external side effects (webhooks, emails, cache invalidations) were triggered.

Operational Effect

  1. **Deterministic Rollback:** Replaces blind snapshot restoration with an executable inverse action, ensuring the external environment (caches, webhooks, APIs) is correctly reconciled.
  2. **Concurrency Safety:** Prevents the rollback from clobbering newer data by verifying the `pre_commit_state_hash` before executing the compensating action.
  3. **Explicit Acknowledgment of Loss:** Forces the instance to document irreversible side effects, ensuring the successor does not falsely assume a clean state.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json


class RollbackVerdict(Enum):
    FULL_ROLLBACK_POSSIBLE = auto()
    PARTIAL_ROLLBACK_REQUIRES_HUMAN = auto()
    BLOCKED_CONCURRENT_MUTATION = auto()


@dataclass(frozen=True)
class CompensatingAction:
    action_type: str
    payload: str
    idempotency_key: str


@dataclass(frozen=True)
class StateMutationRecord:
    mutation_id: str
    pre_commit_state_hash: str
    compensating_action: CompensatingAction
    cascading_side_effects: frozenset[str]
    irreversible_effects: frozenset[str]


@dataclass(frozen=True)
class RollbackManifest:
    record: StateMutationRecord
    verdict: RollbackVerdict
    manifest_digest: str


class RollbackPathGuard:
    """
    Evaluates state mutations to ensure a verified rollback path is preserved
    before the commit is considered final.
    """

    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def seal_rollback_manifest(
        cls, record: StateMutationRecord
    ) -> RollbackManifest:
        
        if not record.compensating_action.idempotency_key.strip():
            raise ValueError(
                "Compensating action lacks an idempotency key. Rollback cannot be safely retried."
            )

        # Gate 1: Irreversible effects force a partial rollback state
        if record.irreversible_effects:
            verdict = RollbackVerdict.PARTIAL_ROLLBACK_REQUIRES_HUMAN
        else:
            verdict = RollbackVerdict.FULL_ROLLBACK_POSSIBLE

        digest_payload = {
            "mutation_id": record.mutation_id,
            "pre_commit_hash": record.pre_commit_state_hash,
            "compensating_action": record.compensating_action.action_type,
            "idempotency": record.compensating_action.idempotency_key,
            "irreversible": tuple(sorted(record.irreversible_effects)),
            "verdict": verdict.name,
        }
        
        digest = cls._hash(
            json.dumps(digest_payload, sort_keys=True, separators=(",", ":"))
        )

        return RollbackManifest(
            record=record,
            verdict=verdict,
            manifest_digest=digest,
        )

    @classmethod
    def verify_rollback_admissibility(
        cls,
        manifest: RollbackManifest,
        current_state_hash: str,
    ) -> tuple[bool, str]:
        """
        Fetching-time check: confirms the environment is safe for the
        compensating action by detecting concurrent mutations.
        """
        if manifest.verdict is RollbackVerdict.PARTIAL_ROLLBACK_REQUIRES_HUMAN:
            return False, (
                f"IRREVERSIBLE_SIDE_EFFECTS: {sorted(manifest.record.irreversible_effects)}. "
                "Human reconciliation required."
            )

        if current_state_hash != manifest.record.pre_commit_state_hash:
            return False, (
                "CONCURRENT_MUTATION_DETECTED: Current state hash does not match "
                "the pre-commit hash. Rollback would clobber newer data."
            )

        return True, (
            f"ROLLBACK_ADMISSIBLE: Execute {manifest.record.compensating_action.action_type} "
            f"with idempotency key {manifest.record.compensating_action.idempotency_key}."
        )