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

# Session Mirror

> Mirror session transcripts to a remote backend without blocking the local turn

A mirror replicates each persisted turn to a remote backend on a background thread, so a session can follow the user across machines — local disk stays the source of truth.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Local-First Session Mirror"
        Turn[💬 Agent Turn] --> Local[💾 Local JSON<br/>~/.praisonai/sessions/]
        Local --> Queue[📤 Bounded Queue]
        Queue -.background.-> Writer[⚙️ Daemon Writer]
        Writer --> Mirror[☁️ Remote Backend]
    end

    classDef turn fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef local fill:#10B981,stroke:#7C90A0,color:#fff
    classDef queue fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef remote fill:#189AB4,stroke:#7C90A0,color:#fff

    class Turn turn
    class Local local
    class Queue,Writer queue
    class Mirror remote
```

## Quick Start

**1. Plug a mirror into DefaultSessionStore**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from typing import Any, Dict, List

from praisonaiagents import Agent
from praisonaiagents.session import DefaultSessionStore, SessionMirrorProtocol


class LoggingMirror:
    """Minimal SessionMirrorProtocol implementation — keeps records in memory."""

    def __init__(self):
        self._store: Dict[str, List[Dict[str, Any]]] = {}

    def append(self, session_id: str, records: List[Dict[str, Any]]) -> None:
        self._store.setdefault(session_id, []).extend(records)
        print(f"[mirror] +{len(records)} record(s) for {session_id}")

    def load(self, session_id: str) -> List[Dict[str, Any]]:
        return list(self._store.get(session_id, []))


store = DefaultSessionStore(mirror=LoggingMirror())
agent = Agent(
    name="Assistant",
    instructions="Be concise.",
    memory={"session_id": "user-42", "session_store": store},
)
agent.start("Remember I like green tea.")
```

The local file at `~/.praisonai/sessions/user-42.json` **and** the mirror both receive the turn.

**2. Continue on another machine**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
store = DefaultSessionStore(mirror=LoggingMirror())
records = store._mirror.load("user-42")  # hydrate a session never seen locally
```

A configured mirror's `load(session_id)` returns the full transcript, so a fresh install can pick up the conversation.

## How It Works

The store writes local disk first and returns; the record is then handed to a bounded queue drained by a daemon thread that calls `mirror.append`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Store as DefaultSessionStore
    participant Disk as ~/.praisonai/sessions/
    participant Queue as Bounded Queue
    participant Writer as Daemon Writer
    participant Mirror as SessionMirrorProtocol

    Agent->>Store: add_message(...)
    Store->>Disk: write JSON (source of truth)
    Disk-->>Store: OK
    Store-->>Agent: return (turn complete)
    Store->>Queue: enqueue(session_id, [record])
    Note over Store,Queue: never blocks the caller
    Queue-->>Writer: drain in background
    Writer->>Mirror: append(session_id, records)
    alt success
        Mirror-->>Writer: OK
    else transient failure
        Writer->>Writer: backoff 50ms → 1s, retry ×3
        Writer->>Mirror: append(...)
    else permanent failure
        Writer->>Writer: log error, move on
        Note over Writer,Disk: local file is authoritative
    end
```

The failure modes never touch the local write:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TD
    A[Record persisted locally] --> B{Enqueue}
    B -->|queue full| D[Drop-to-log<br/>run `session sync` later]
    B -->|ok| C[Daemon append]
    C -->|success| E[Mirrored]
    C -->|transient| F[Retry ×3 with backoff]
    F -->|still failing| G[Drop-to-log]
    C -->|permanent| G

    classDef local fill:#10B981,stroke:#7C90A0,color:#fff
    classDef queue fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef remote fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef drop fill:#8B0000,stroke:#7C90A0,color:#fff

    class A,E local
    class B,C,F queue
    class D,G drop
```

## Configuration Options

### `SessionMirrorProtocol` methods

The `runtime_checkable` protocol lives at `praisonaiagents.session.SessionMirrorProtocol`.

| Method | Required? | Signature | Contract |
| - | - | - | - |
| `append` | ✅ | `(session_id: str, records: List[Dict[str, Any]]) -> None` | Persist the newly-produced turn records. Each record is already timestamped and id-tagged, so a re-append is idempotent (last-writer per record id) — no merge logic required. |
| `load` | ✅ | `(session_id: str) -> List[Dict[str, Any]]` | Full ordered transcript for a session (empty list if unknown). Used to hydrate a session-id that was never seen locally. |
| `list_sessions` | ⚪ optional | `(*, user_id: Optional[str] = None) -> List[Dict[str, Any]]` | Enumerate mirrored sessions for a future `session list --remote` surface. |

### `DefaultSessionStore(mirror=…)`

| Symbol | Type / Default | Description |
| - | - | - |
| `mirror` | `Optional[SessionMirrorProtocol]` / `None` | Wire any mirror into the default store. Left `None` → **zero overhead, no background thread**. |
| `flush_mirror(timeout=None)` | `bool` | Block until queued mirror records are processed. Returns `True` immediately when `mirror is None`. |
| `close_mirror()` | `None` | Stop the daemon writer, draining queued records first. |
| `DEFAULT_MIRROR_QUEUE_SIZE` | `1000` | Bound of the background `queue.Queue`. On overflow, records are dropped-to-log. |
| `DEFAULT_MIRROR_MAX_RETRIES` | `3` | `append` retries per record with 50ms → 1s exponential backoff before logging and moving on. |

## Writing a Mirror

A mirror is three tiny methods — the `LoggingMirror` from Quick Start is the whole story.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from typing import Any, Dict, List


class LoggingMirror:
    def __init__(self):
        self._store: Dict[str, List[Dict[str, Any]]] = {}

    def append(self, session_id: str, records: List[Dict[str, Any]]) -> None:
        self._store.setdefault(session_id, []).extend(records)

    def load(self, session_id: str) -> List[Dict[str, Any]]:
        return list(self._store.get(session_id, []))
```

> [!NOTE]
>
> Concrete wrapper-side adapters (postgres/supabase/turso/sqlite) land in `praisonai.persistence.conversation.*` alongside store-unification (#3645). Until then, implement the protocol directly against your backend.

## Local-First Guarantees

> [accordion] Local disk write always happens first
>
> The mirror is fed *after* the message is persisted to the session file, off the lock. A mirror outage never reaches the caller as a failure.

> [accordion] A slow/broken mirror never blocks a turn
>
> Records are enqueued onto a bounded `queue.Queue` (`DEFAULT_MIRROR_QUEUE_SIZE = 1000`) drained by a daemon thread (`praisonai-session-mirror`).

> [accordion] Queue overflow is drop-to-log, not deadlock
>
> On `Queue.Full`, records are dropped with a warning pointing operators at the (future) `session sync` reconciler; the local file is unaffected.

> [accordion] Transient failures retry with backoff
>
> `_SessionMirrorWriter._flush_one` retries `DEFAULT_MIRROR_MAX_RETRIES = 3` times with 50ms → 1s exponential backoff before logging and moving on.

> [accordion] mirror=None is byte-for-byte the old store
>
> The writer is only instantiated when `mirror is not None` — no queue, no thread, no import cost.

> [accordion] Post-close appends are rejected
>
> After `close_mirror()`, further enqueues drop-to-log rather than leaving records stranded with no consumer.

## Testing & Reconciliation

Use `flush_mirror(timeout=…)` to make mirroring deterministic in a test and `close_mirror()` to drain in a teardown.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
store = DefaultSessionStore(mirror=my_mirror)
# ... run turns ...
assert store.flush_mirror(timeout=2.0), "mirror did not drain in time"
store.close_mirror()   # stops the daemon writer after draining
```

> [!NOTE]
>
> The CLI-facing `session sync` reconciler is not yet shipped (blocked on #3645). Until then, `flush_mirror` is the deterministic seam for reconciliation and tests.

## Backward Compatibility

`mirror=None` is the default — existing `DefaultSessionStore(...)` call sites are byte-for-byte unchanged, with no daemon thread spawned and no dependency added.

## Best Practices

> [accordion] Keep append idempotent
>
> Records carry stable ids, so re-appending the same record is last-writer-wins. Don't add merge logic — rely on the id.

> [accordion] Don't raise on transient failure
>
> The writer retries with backoff and drops-to-log on permanent failure. Let it — raising in `append` just burns a retry.

> [accordion] Never do heavy work in append
>
> `append` runs on the single writer thread. Keep it a fast network/DB write so the queue drains and doesn't back up.

> [accordion] Always call close\_mirror() on shutdown
>
> `close_mirror()` drains in-flight records before stopping the daemon writer. Skipping it can drop the last queued turns.

## Related

> [card] [Session Store](https://praison.ai/docs/features/session-store.md)
>
> The default JSON store the mirror wires into

> [card] [Cross-Session Recall](https://praison.ai/docs/features/cross-session-recall.md)
>
> Search past sessions — hydrate non-local ids from a mirror

> [card] [Sessions & Remote Agents](https://praison.ai/docs/features/sessions.md)
>
> Stateful conversations across restarts

> [card] [Gateway Session Persistence](https://praison.ai/docs/features/gateway-session-persistence.md)
>
> Persist gateway sessions across restarts


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.