> ## Documentation Index
> Fetch the complete documentation index at: https://docs.manthan.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Replay Protection

> How Parmana prevents duplicate execution using executionId and the replay store

## The problem

A payment authorization is captured, intercepted, and re-submitted. Without replay protection, the governance system would approve it again — producing a second valid attestation for the same transaction.

Replay protection ensures each execution happens exactly once.

***

## How it works

Every `POST /execute` request includes an `executionId`. Before any evaluation happens, the runtime checks the replay store:

* If the `executionId` has never been seen: proceed, mark it as `RESERVED`
* If the `executionId` is in a terminal state (`CONFIRMED`, `OVERRIDDEN`): throw `[INV-013@replay]`
* If the `executionId` is in a transient state (`RESERVED`, `EXECUTING`, `FAILED`, `EXPIRED`): depends on whether the state is retryable

The replay key used in the store is the `executionId` you provide. This is different from the `execution_fingerprint` (the semantic hash of the inputs) — two different `executionId` values can have the same `execution_fingerprint` if they present identical policy and signals.

***

## Replay state machine

```mermaid theme={null}
stateDiagram-v2
    [*] --> RESERVED: reserve(executionId)
    RESERVED --> EXECUTING: startExecution(executionId)
    EXECUTING --> CONFIRMED: confirm(executionId)
    EXECUTING --> OVERRIDDEN: override(executionId)
    RESERVED --> FAILED: fail(executionId)
    EXECUTING --> FAILED: fail(executionId)
    FAILED --> EXPIRED: expire(executionId) or TTL
    RESERVED --> EXPIRED: expire(executionId) or TTL
```

States:

| State        | Meaning                                        |
| ------------ | ---------------------------------------------- |
| `RESERVED`   | Execution slot claimed, evaluation in progress |
| `EXECUTING`  | Token issued, signing in progress              |
| `CONFIRMED`  | Execution complete — terminal                  |
| `OVERRIDDEN` | Override approved — terminal                   |
| `FAILED`     | Execution failed — retryable after TTL         |
| `EXPIRED`    | TTL elapsed — retryable                        |

`CONFIRMED` and `OVERRIDDEN` are terminal. An `executionId` in either state will never execute again.

***

## Replay store implementations

### MemoryReplayStore (development only)

```typescript theme={null}
import { MemoryReplayStore } from "@parmanasystems/core";

const store = new MemoryReplayStore({
  warnInProduction: true,        // logs a warning if NODE_ENV === "production"
  maxSize: 1_000_000,            // max entries before throwing
  reservationTtlSeconds: 300,    // TTL for RESERVED state
  failedTtlSeconds: 30,          // TTL for FAILED state
});
```

<Warning>
  `MemoryReplayStore` does not persist across process restarts and does not work across multiple instances. Use it only for development and single-process integration tests.
</Warning>

### RedisReplayStore (production)

```typescript theme={null}
import { RedisReplayStore } from "@parmanasystems/core";

const store = new RedisReplayStore(
  "redis://localhost:6379",   // Redis URL — required
  {
    reservationTtlSeconds: 300,  // default
    failedTtlSeconds: 30,        // default
  }
);
```

Redis stores replay state durably. State survives process restarts and is shared across multiple server instances.

**Redis is required for production.** The server will not start without a reachable `REDIS_URL`.

***

## Implementing the ReplayStore interface

You can implement `ReplayStore` for any backend:

```typescript theme={null}
import type { ReplayStore, ReplayState } from "@parmanasystems/core";

class DynamoReplayStore implements ReplayStore {
  async reserve(executionId: string): Promise<void> { ... }
  async startExecution(executionId: string): Promise<void> { ... }
  async confirm(executionId: string): Promise<void> { ... }
  async fail(executionId: string): Promise<void> { ... }
  async override(executionId: string): Promise<void> { ... }
  async expire(executionId: string): Promise<void> { ... }
  async getReplayState(executionId: string): Promise<ReplayState | null> { ... }
  async hasExecuted(executionId: string): Promise<boolean> { ... }
  async markExecuted(executionId: string): Promise<void> { ... }
}
```

***

## What happens on replay

When an `executionId` is already in a terminal state (`CONFIRMED` or `OVERRIDDEN`):

```json theme={null}
{
  "error": "[INV-013@replay] Replay detected: execution_fingerprint a3f8... has already been consumed"
}
```

HTTP status: 422. Do not retry with the same `executionId`. If you need to re-evaluate (different signals or policy version), use a new `executionId`.

***

## Troubleshooting

**Legitimate retry blocked by replay** — If your application retried a failed request with the same `executionId`, and the first attempt succeeded, the retry is correctly blocked. Check the audit database for the existing decision. Do not retry authority verification outcomes with the same ID.

**`[SYS-REPLAY-001] REDIS_URL is required`** — Redis is not configured. The server will not start. Set `REDIS_URL` in your environment.

**FAILED state not recovering** — The FAILED TTL defaults to 30 seconds. After 30 seconds, the state transitions to EXPIRED and the `executionId` can be retried. If you want to retry sooner, use a different `executionId`.

**State appears stuck in RESERVED** — The reservation TTL defaults to 300 seconds. If the server crashed between `RESERVED` and `EXECUTING`, the slot expires after 5 minutes. After expiry, the `executionId` can be retried.
