> ## 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 Persistence

> Automatic session persistence with zero configuration

Provide a `session_id` on your agent and conversation history is saved and restored automatically — no database setup required.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Reviewer",
    instructions="Review the codebase and answer follow-ups",
    memory={"session_id": "review-2026-06-25"},
)
agent.start("Summarise the auth module")
```

The user returns with the same `session_id`; prior turns reload automatically from disk.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Session Persistence"
        A[🤖 Agent] --> S[💾 session_id]
        S --> D[📁 JSON / DB]
        D --> R[⏪ Restore on next run]
    end

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef store fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class S,D store
    class R result
```

<Note>
  Upgrade if you rely on multi-process saves, gateway resume, or idempotent `save_state()` — fixes landed in PRs #1709, #1897, #1972, and #2102.
</Note>

## Quick Start

<Steps>
  <Step title="Start with a session_id">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    agent = Agent(
        name="Reviewer",
        instructions="Review the codebase and answer follow-ups",
        memory={"session_id": "review-2026-06-25"},
    )
    agent.start("Summarise the auth module")
    ```
  </Step>

  <Step title="Resume from the CLI">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    praisonai session resume review-2026-06-25 "Now suggest test cases for the same module"
    ```

    History, model, and agent name are restored — not just the transcript.
  </Step>
</Steps>

***

## Agent Session Persistence

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# First conversation
agent = Agent(
    name="Assistant",
    memory={"session_id": "my-session-123"}
)
agent.start("My name is Alice and I love pizza")

# Later, in a new process - history is restored automatically
agent = Agent(
    name="Assistant", 
    memory={"session_id": "my-session-123"}
)
agent.start("What is my name?")  # Agent remembers: "Alice"
```

## Deterministic Resume

As of [PR #2277](https://github.com/MervinPraison/PraisonAI/pull/2277), `praisonai session resume` is a first-class restore — not a transcript display.

What is restored:

* **Chat history** — full conversation messages
* **Model** — the LLM used in the session (read from `metadata["model"]`, falls back to `metadata["llm"]`)
* **Agent name** — the name of the agent that ran the session

New CLI options:

* `praisonai session resume <id>` — restores state and shows a "Session Resumed" panel
* `praisonai session resume <id> "<prompt>"` — restores state and continues with a new prompt
* `praisonai session resume <id> --transcript` — opt-in to the old transcript-only view (panel title: "Session Transcript")

Session lookup checks the project store first, then falls back to the global default store — so sessions created via `praisonai run --continue` or via the gateway/TUI are all reachable.

For the full CLI reference, see [Session Command](/docs/cli/session#resume-a-session).

When context compaction is enabled, resume gets cheaper still: the compaction summary is persisted to the session and replayed on the next run, so `--continue` / `session resume` reconstructs a compacted working history (summary + retained tail) instead of the full raw transcript.

<Note>
  With `execution=ExecutionConfig(context_compaction=True)` and a bound `session_id`, resume automatically uses the compacted working history. Sessions without a checkpoint resume from raw messages exactly as before. See [Compacted Session Resume](/docs/features/session-compaction-checkpoint).
</Note>

***

## How It Works

When you provide a `session_id` to an Agent:

1. **Automatic Persistence**: Conversation history is automatically saved to disk after each message
2. **Automatic Restoration**: When a new Agent is created with the same `session_id`, history is restored
3. **Zero Configuration**: No database setup required - uses JSON files by default

### Default Storage Location

Sessions are stored in: `~/.praisonai/sessions/{session_id}.json`

## Behavior Matrix

<Note>
  **Session expiry / cleanup.** This page covers the low-level `session_id` + `DefaultSessionStore` path used by `Agent(memory={"session_id": ...})`. If you instead use the high-level `Session(...)` wrapper, it supports `session_ttl`, `is_expired()`, `time_to_expiry()`, and `close()` — see [Sessions & Remote Agents → Session Expiry & Cleanup](/features/sessions#session-expiry--cleanup).
</Note>

| Scenario                             | Behavior                     |
| ------------------------------------ | ---------------------------- |
| `session_id` provided, no DB         | JSON persistence (automatic) |
| `session_id` provided, with DB       | DB adapter used              |
| No `session_id`, same Agent instance | In-memory only               |
| No `session_id`, new Agent instance  | No history                   |

## In-Memory Memory (Default)

Even without `session_id`, the same Agent instance remembers previous messages:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="Assistant")

# First message
agent.chat("My favorite number is 42")

# Second message - agent remembers
agent.chat("What is my favorite number?")  # Agent responds: "42"
```

<Note>
  In-memory memory is lost when the Agent instance is garbage collected or the process ends.
  Use `session_id` for persistence across processes.
</Note>

## Persistent Sessions

### Basic Usage

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# Create agent with session_id
agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
    memory={"session_id": "user-123-chat"}
)

# Conversation is automatically persisted
response = agent.chat("Remember that my birthday is January 15th")
```

### Resuming Sessions

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# In a new Python process or after restart
from praisonaiagents import Agent

# Same session_id restores history
agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
    memory={"session_id": "user-123-chat"}
)

# Agent remembers previous conversation
response = agent.chat("When is my birthday?")
# Agent responds: "Your birthday is January 15th"
```

### Session File Format

Sessions are stored as JSON files with automatic metadata tracking:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "session_id": "user-123-chat",
  "messages": [
    {"role": "user", "content": "Remember my birthday is January 15th", "timestamp": 1704153600.0},
    {"role": "assistant", "content": "I'll remember that!", "timestamp": 1704153601.5}
  ],
  "created_at": "2026-01-02T04:00:00+00:00",
  "updated_at": "2026-01-02T04:01:00+00:00",
  "agent_name": "Assistant",
  "model": "gpt-4o",
  "total_tokens": 125,
  "cost": 0.0032,
  "agent_id": "assistant-001",
  "source": "chat"
}
```

### Session Metadata Fields

The following metadata is automatically populated after each assistant turn:

| Field          | Type     | Description                             |
| -------------- | -------- | --------------------------------------- |
| `model`        | `string` | LLM model used in the session           |
| `total_tokens` | `int`    | Cumulative input+output tokens          |
| `cost`         | `float`  | Estimated USD cost                      |
| `agent_id`     | `string` | Gateway or registry agent id            |
| `source`       | `string` | Origin: `chat`, `gateway`, `cli`, `api` |
| `agent_name`   | `string` | Human-readable agent name               |

These fields enable cost tracking and usage analytics across sessions.

#### How metadata is populated

After every assistant turn, `praisonaiagents/agent/memory_mixin.py::_persist_session_stats()` calls `store.update_session_metadata(session_id, model=..., total_tokens=..., cost=..., source=..., agent_id=...)`. You normally don't call this directly — but you **can** call it to record custom metadata on a session:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.session import get_default_session_store

store = get_default_session_store()
store.update_session_metadata(
    "user-123-chat",
    custom_field="my value",
    model="gpt-4o-mini",
    total_tokens=125,
)
```

## Idempotent saves

`Session._save_agent_chat_histories()` uses `set_chat_history(session_id, messages)` to atomically replace the persisted history rather than appending. This means:

* Repeated `session.save_state()` calls do not duplicate messages.
* The per-turn `_persist_message()` path and the `_auto_save_session()` flush share `_auto_save_last_index`, so each message is written exactly once.

Custom session stores that do not implement `set_chat_history` fall back to `add_message()` with a logged warning — those stores may produce duplicates on repeated `save_state()` calls until they add `set_chat_history`.

## Multi-Process Safety

The session store is safe under concurrent multi-process and multi-instance use on **both reads and writes**:

* **Atomic writes** — every mutator (`add_message`, `set_agent_info`, `set_gateway_info`, `clear_session`, `update_session_metadata`) reloads the session from disk inside `FileLock`, mutates, then atomically writes (temp file + `os.replace`). Concurrent writers cannot drop each other's messages.
* **Fresh reads** — `get_chat_history`, `get_session`, and `get_sessions_by_agent` reload from disk under `FileLock` on every call and refresh the in-process cache. Two store instances pointing at the same `session_dir` will always see each other's writes.
* **Cross-platform locks** — `fcntl.flock` on Unix/macOS, `msvcrt.locking` on Windows.

<Note>
  The `praisonai session` CLI has its own session store (`praisonai.cli.session.UnifiedSessionStore`, separate from `praisonaiagents.session.DefaultSessionStore` documented above). Both stores use the same cross-platform locking strategy as of [PR #1837](https://github.com/MervinPraison/PraisonAI/pull/1837). `UnifiedSessionStore` now reloads and merges under exclusive lock — shared message-prefix merge + delta-based stats merge — as of [PR #1885](https://github.com/MervinPraison/PraisonAI/pull/1885). Updated in [PR #1892](https://github.com/MervinPraison/PraisonAI/pull/1892): `UnifiedSessionStore.save()` reloads under lock and merges concurrent writes (previously it overwrote with the in-process cache, dropping messages from a second process that wrote between load and save). `UnifiedSessionStore.load()` always reads from disk. The Windows code path locks the entire file (`max(file_size, 1)` bytes via `msvcrt.locking`) instead of only the first byte, matching Unix `fcntl.flock` semantics. Concurrent writers from a TUI + `--interactive` session, or from two terminals sharing `~/.praisonai/sessions/`, no longer drop each other's messages. See [CLI Sessions](/docs/cli/session#cross-platform-support) for the CLI-side details.
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant CLI_A as Process A (save)
    participant Disk as session-1.json
    participant CLI_B as Process B (save)

    CLI_A->>Disk: FileLock acquire
    CLI_A->>Disk: reload, merge ["hi from A"]
    CLI_A->>Disk: atomic write
    CLI_A->>Disk: FileLock release
    CLI_B->>Disk: FileLock acquire
    CLI_B->>Disk: reload (sees "hi from A"), merge ["hi from B"]
    CLI_B->>Disk: atomic write
    CLI_B->>Disk: FileLock release
    Note over Disk: Final file contains both messages
    
    classDef process fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef file fill:#189AB4,stroke:#7C90A0,color:#fff
    
    class CLI_A,CLI_B process
    class Disk file
```

<Note>
  Multiple processes (a gateway worker and a bot worker, several uvicorn workers, a CLI alongside a server) can safely share the same `session_dir`. Each call to `get_chat_history` returns the latest committed state on disk — there is no stale-cache window.
</Note>

<Note>
  `store.invalidate_cache(session_id)` still exists for backwards compatibility, but since reads always reload from disk it is effectively a no-op on the read path. You no longer need to call it before `get_chat_history` / `get_session`.
</Note>

## Direct Session Store Access

For advanced use cases, you can access the session store directly:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.session import get_default_session_store

store = get_default_session_store()

# Add messages
store.add_user_message("session-123", "Hello")
store.add_assistant_message("session-123", "Hi there!")

# Get history
history = store.get_chat_history("session-123")
# [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi there!"}]

# List all sessions
sessions = store.list_sessions()

# Delete a session
store.delete_session("session-123")
```

### Custom Session Directory

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.session import DefaultSessionStore

store = DefaultSessionStore(
    session_dir="/custom/path/sessions",
    max_messages=200,        # Default: 100
    lock_timeout=10.0,       # Default: 5.0 seconds
    retention="compact",     # "compact" (default) | "truncate" | "keep_all"
    active_window=200,       # Recent turns kept live; defaults to max_messages
)
```

## Using with DB Adapter

When a DB adapter is provided, it takes precedence over JSON persistence. The `DbSessionAdapter` now persists both messages and metadata to the conversation store, ensuring session metadata survives process restarts.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# Custom DB adapter (e.g., PostgreSQL, MongoDB)
class MyDbAdapter:
    def on_agent_start(self, agent_name, session_id, user_id=None, metadata=None):
        # Load history from database
        return []
    
    def on_user_message(self, session_id, content):
        # Save user message to database
        pass
    
    def on_agent_message(self, session_id, content):
        # Save agent message to database
        pass

agent = Agent(
    name="Assistant",
    memory={"session_id": "my-session"},
    db=MyDbAdapter()
)
```

<Note>
  For DB-backed sessions, `clear_session()` and `delete_session()` now purge persisted messages from the database via the conversation store's `delete_messages()` method, ensuring that cleared history does not reappear after restarts.

  When using the built-in `DbSessionAdapter` (via `praisonai.db`), both messages and metadata are automatically persisted to your database. The `set_metadata()` and `get_metadata()` methods now round-trip through the conversation store, so metadata survives process restarts without additional configuration. For complete examples, see the [HostedAgent persistence guide](/docs/features/managed-agent-persistence).
</Note>

## Context Caching

For cost optimization with Anthropic models, use `caching=True`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
agent = Agent(
    name="Assistant",
    memory={"session_id": "my-session"},
    caching=True,  # Enables Anthropic prompt caching
)
```

This caches the system prompt, reducing token costs for repeated conversations.

## Bot Session Persistence

Bots use the **same session store** as agents. Each user gets a persistent session that survives bot restarts.

Configure via `bot.yaml`:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
channels:
  telegram:
    token: ${TELEGRAM_TOKEN}
    session:
      max_history: 50
      reset:
        mode: idle
        idle_minutes: 30
```

Run your bot:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai.bots import TelegramBot

bot = TelegramBot(
    token="YOUR_BOT_TOKEN",
    agent=Agent(name="Assistant", instructions="Help users."),
)
bot.run()
```

### max\_history Resolution

When a bot starts, it resolves `max_history` using this precedence ladder:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    A[channel-level max_history in YAML] -->|set?| D[Use channel max_history]
    A -->|not set| B[session.max_history in YAML]
    B -->|set?| E[Use session.max_history]
    B -->|not set| C[Default: 100]
    D --> CP{session.compaction.enabled?}
    E --> CP
    C --> CP
    CP -->|No| TR[Tail-slice truncate]
    CP -->|Yes| CO[Compact older turns<br/>max_history × 4 hard cap]

    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef fallback fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    classDef compact fill:#8B0000,stroke:#7C90A0,color:#fff

    class A,B config
    class C fallback
    class D,E result
    class CP fallback
    class TR result
    class CO compact
```

### Bot Session Configuration

Configure these settings in your `bot.yaml` under each channel:

| Setting                      | YAML key                          | Type     | Default       | Description                                                            |
| ---------------------------- | --------------------------------- | -------- | ------------- | ---------------------------------------------------------------------- |
| Per-channel history limit    | `max_history`                     | `int`    | `100`         | Highest precedence. Caps messages kept per user.                       |
| Session-scoped history limit | `session.max_history`             | `int`    | `100`         | Used when `max_history` is not set.                                    |
| Session reset mode           | `session.reset.mode`              | `string` | `"none"`      | `none`, `idle`, `daily`, or `both`.                                    |
| Idle reset threshold         | `session.reset.idle_minutes`      | `int`    | `60`          | Minutes of inactivity before reset (when mode includes `idle`).        |
| Daily reset hour             | `session.reset.at_hour`           | `int`    | —             | Hour (0–23) for daily reset (required when mode includes `daily`).     |
| Compaction enabled           | `session.compaction.enabled`      | `bool`   | `false`       | Master switch for summarising older turns.                             |
| Compaction strategy          | `session.compaction.strategy`     | `string` | `"summarize"` | `truncate`, `sliding`, `summarize`, `smart`, `prune`, `llm_summarize`. |
| Compaction message threshold | `session.compaction.max_messages` | `int`    | `100`         | Approximate threshold (converted to a token budget).                   |
| Compaction token budget      | `session.compaction.max_tokens`   | `int`    | —             | Optional explicit budget. Overrides `max_messages`.                    |
| Recent tail kept verbatim    | `session.compaction.keep_recent`  | `int`    | `10`          | Most-recent messages kept un-summarised.                               |

<Note>
  When compaction is enabled, `max_history` becomes a hard upper bound (`max_history × 4`) instead of the primary trim mechanism. See [Bot Session Compaction](/features/bot-session-compaction) for the full flow.
</Note>

<Note>
  `max_history` at the channel level takes precedence over `session.max_history`. Use `session.max_history` for new configs — it is the preferred form.
</Note>

### Session Reset Policies

| Mode    | Behavior                                                        |
| ------- | --------------------------------------------------------------- |
| `none`  | Sessions never auto-reset (default).                            |
| `idle`  | Resets after `idle_minutes` of inactivity.                      |
| `daily` | Resets once per day at `at_hour` (0–23).                        |
| `both`  | Resets on idle **or** on daily schedule, whichever comes first. |

<Note>
  Without a `store` parameter, `BotSessionManager` falls back to in-memory-only mode for backward compatibility.
</Note>

### Bounded lock caches

Long-running bots keep concurrency safe without unbounded memory growth:

| Lock type      | Scope                                    | Cleanup                                                                               |
| -------------- | ---------------------------------------- | ------------------------------------------------------------------------------------- |
| Agent locks    | One lock per agent instance              | Tied to agent lifetime — auto-removed when the agent is garbage-collected             |
| Per-user locks | Debounce, session, and run-control paths | Bounded LRU cache (max 10,000 entries, 1 hour TTL) — idle entries evict automatically |

Recreating agents per request is safe: locks no longer share stale `id(agent)` keys across unrelated users.

## Session Store Protocol

All session stores implement `SessionStoreProtocol` — a lightweight interface that enables swapping backends:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.session import SessionStoreProtocol

# Any class with these 5 methods satisfies the protocol:
# add_message(), get_chat_history(), clear_session(),
# delete_session(), session_exists()

assert isinstance(get_default_session_store(), SessionStoreProtocol)
```

## Retention Policies

Long conversations stay safe — older turns are summarised and archived, not silently dropped.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# Long conversations are now safe — older turns are summarised and archived,
# not silently dropped. This is the default behaviour.
agent = Agent(
    name="assistant",
    instructions="Answer helpfully, remembering everything the user has told you.",
    memory={"session_id": "my-long-chat"},
)

# 100+ turns later, the earliest questions the user asked are still
# accessible — as a summary in the active window, and raw in the archive.
agent.start("What was the first thing I asked you?")
```

Before this change, the 101st turn silently deleted the 1st on every write. Retention policies (Issue #2709) fix that: overflow beyond the active window is rolled up into one summary message and preserved raw in a durable `archived_messages` field.

### The Three Policies

| Policy                | Behaviour                                                                                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"compact"` (default) | Summarise the oldest overflow turns into **one** synthetic summary message and archive the raw turns in `archived_messages` — nothing is silently dropped. |
| `"truncate"`          | Legacy behaviour — hard-slice to the tail; older turns are permanently dropped.                                                                            |
| `"keep_all"`          | Never trim the active window (unbounded history).                                                                                                          |

Built-in defaults from `store.py`: `DEFAULT_RETENTION = "compact"` and `DEFAULT_MAX_MESSAGES = 100`.

### What Non-Destructive Means

Overflow becomes one summary message plus a raw archive — the active window stays small while the full record survives.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Compact Retention"
        A[📚 New turn] --> B{🔍 Over active window?}
        B -->|No| C[✅ Append]
        B -->|Yes| D[📝 Summarise oldest]
        D --> E[💾 Archive raw]
        E --> C
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff

    class A input
    class B,D,E process
    class C output
```

### Choosing a Policy

Pick the policy that matches what you care about most.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{What matters most?} --> A[Keep every turn — never lose context]
    Q --> B[Bounded disk usage — hard cap]
    Q --> C[Unlimited growth — I compact externally]

    A --> A1["retention='compact' (default)"]
    B --> B1["retention='truncate'"]
    C --> C1["retention='keep_all'"]

    classDef question fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef choice fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef answer fill:#10B981,stroke:#7C90A0,color:#fff

    class Q question
    class A,B,C choice
    class A1,B1,C1 answer
```

### Configuration

Configure retention three ways — from zero-code env vars up to the constructor.

**Level 1 — env vars (zero code):**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
export PRAISONAI_SESSION_RETENTION=compact       # default
export PRAISONAI_SESSION_ACTIVE_WINDOW=200       # keep 200 recent turns live
```

**Level 2 — constructor:**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.session import DefaultSessionStore

store = DefaultSessionStore(
    retention="compact",     # or "truncate", or "keep_all"
    active_window=200,       # optional; defaults to max_messages
)
```

**Level 3 — legacy behaviour (backward-compat trap):**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Passing max_messages != 100 with NO explicit retention keeps the
# LEGACY truncate behaviour. This is intentional backward compatibility.
# Set retention="compact" explicitly to opt into non-destructive rollup.
store = DefaultSessionStore(max_messages=50)  # -> truncate
store = DefaultSessionStore(max_messages=50, retention="compact")  # -> compact
```

<Warning>
  Passing `max_messages=N` with `N != 100` and **no** explicit `retention` keeps the legacy `truncate` behaviour — older turns are permanently dropped. To opt into non-destructive rollup, pass `retention="compact"` explicitly.
</Warning>

### Environment Variables

Both env vars are read only when the global default store is **first** constructed (lazy singleton). Setting them mid-process has no effect.

| Env var                           | Type     | Values                                | Effect                                                                                                                |
| --------------------------------- | -------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `PRAISONAI_SESSION_RETENTION`     | `string` | `compact` \| `truncate` \| `keep_all` | Overrides the global default store's retention policy. Invalid values are logged and the default (`compact`) is used. |
| `PRAISONAI_SESSION_ACTIVE_WINDOW` | `int`    | any positive int                      | Overrides the number of recent turns kept live in `messages`. Non-int values are logged and ignored.                  |

### Constructor Parameters

| Parameter       | Type  | Default                                                                        | Description                                                                               |
| --------------- | ----- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `retention`     | `str` | `None` → `compact` (or `truncate` when a non-default `max_messages` is passed) | Overflow policy: `compact`, `truncate`, or `keep_all`. Invalid values raise `ValueError`. |
| `active_window` | `int` | `None` → `max_messages`                                                        | Number of recent turns kept live in `messages`. Tune separately from `max_messages`.      |

### The `archived_messages` Field

Raw archived turns are preserved on disk in `SessionData.archived_messages` and loaded back into `SessionData` / `ExtendedSessionData` on subsequent reads — old session JSON without the field loads cleanly as an empty list. Once the archive crosses `ARCHIVE_WARN_THRESHOLD` (`10_000` entries) the store logs a one-off warning (`archived_messages has grown to N entries`) so operators can spot runaway sessions.

### Reads Return Full History

`get_chat_history()` no longer silently re-caps reads at the legacy 100-message tail — full stored history returns unless you pass `max_messages` explicitly.

### Retention in Practice

A compact session summarises overflow on write and hands back summary + recent turns on resume.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Store as DefaultSessionStore

    Note over Store: retention="compact", active_window=100
    User->>Agent: Turn 100
    Agent->>Store: save(session)
    Store->>Store: _enforce_window → within window, append
    User->>Agent: Turn 101
    Agent->>Store: save(session)
    Store->>Store: _enforce_window → summarise oldest, archive raw
    Note right of Store: log: "compacted 1 early turns into a summary"
    User->>Agent: (later) resume session
    Agent->>Store: get_chat_history()
    Store-->>Agent: summary + recent turns
    Agent-->>User: continues with context preserved
```

<Warning>
  Upgrading is safe. Old session JSON files load cleanly — a missing `archived_messages` defaults to `[]`. Callers that pass `max_messages=N` keep the legacy `truncate` behaviour by default. To opt into non-destructive rollup, either omit `max_messages` (the store uses `DEFAULT_MAX_MESSAGES=100` + `retention="compact"`) or pass `retention="compact"` explicitly.
</Warning>

### Retention Best Practices

<AccordionGroup>
  <Accordion title="Keep the default (compact) unless you have a specific reason to drop history">
    Non-destructive rollup is the right default for `--continue`, `resume`, and `Agent(session_id=...)` flows — the earliest turns survive as a summary plus a raw archive.
  </Accordion>

  <Accordion title="Use truncate for strict FIFO caps on shared/CI machines">
    When you want a hard cap on disk usage and don't care about old turns, `retention="truncate"` hard-slices to the tail.
  </Accordion>

  <Accordion title="Use keep_all only for short-lived sessions or when you compact externally">
    `keep_all` never trims — `archived_messages` and the active window grow unbounded otherwise.
  </Accordion>

  <Accordion title="Tune active_window separately from max_messages">
    A larger `active_window` keeps more live context per turn but raises token cost. It defaults to `max_messages` when unset.
  </Accordion>

  <Accordion title="Watch for the archived_messages warning in logs">
    The store logs once when the archive crosses `ARCHIVE_WARN_THRESHOLD` (`10_000`) so operators can spot runaway sessions.
  </Accordion>
</AccordionGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Use meaningful session IDs">
    Include user or context in the id: `f"user-{user_id}-{conversation_type}"`.
  </Accordion>

  <Accordion title="Respect the default message limit">
    Default `max_messages` is 100. Overflow is summarised and archived by default (`retention="compact"`) — see [Retention Policies](#retention-policies).
  </Accordion>

  <Accordion title="Clean up unused sessions">
    Call `store.delete_session()` to remove stale sessions and purge DB rows when using a DB adapter.
  </Accordion>

  <Accordion title="Enable prompt caching for Anthropic">
    Set `caching=True` on the agent to reduce token cost on repeated conversations.
  </Accordion>
</AccordionGroup>

## API Reference

### Agent Parameters

| Parameter        | Type        | Description                                |
| ---------------- | ----------- | ------------------------------------------ |
| `session_id`     | `str`       | Session identifier for persistence         |
| `db`             | `DbAdapter` | Optional database adapter (overrides JSON) |
| `prompt_caching` | `bool`      | Enable Anthropic prompt caching            |

### DefaultSessionStore Methods

| Method                                                       | Description                                                                                 |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `add_message(session_id, role, content, metadata)`           | Add a message (reload-under-lock)                                                           |
| `add_user_message(session_id, content)`                      | Convenience wrapper for `add_message(role="user", ...)`                                     |
| `add_assistant_message(session_id, content)`                 | Convenience wrapper for `add_message(role="assistant", ...)`                                |
| `get_chat_history(session_id, max_messages)`                 | Get chat history (disk-fresh on every call)                                                 |
| `get_session(session_id)`                                    | Get full session data (disk-fresh on every call)                                            |
| `set_agent_info(session_id, agent_name, user_id)`            | Attach agent name / user id (reload-under-lock)                                             |
| `set_gateway_info(session_id, gateway_session_id, agent_id)` | Link a session to a gateway session id and agent id                                         |
| `update_session_metadata(session_id, **fields)`              | Merge run stats / metadata fields. Safe concurrently across processes. Skips `None` values. |
| `get_by_gateway_session(gateway_session_id)`                 | Look up a session by its gateway session id                                                 |
| `get_sessions_by_agent(agent_name, limit)`                   | List sessions belonging to an agent (each loaded disk-fresh)                                |
| `clear_session(session_id)`                                  | Clear all messages (reload-under-lock)                                                      |
| `delete_session(session_id)`                                 | Delete session completely                                                                   |
| `list_sessions(limit)`                                       | List all sessions                                                                           |
| `session_exists(session_id)`                                 | Check if session exists                                                                     |
| `invalidate_cache(session_id)`                               | Drop the in-memory cache entry (no-op for reads; reads always reload)                       |

***

## Related

<CardGroup cols={2}>
  <Card title="Session Protocol" icon="plug" href="/docs/features/session-protocol">
    Build custom session backends
  </Card>

  <Card title="Bot Session Compaction" icon="compress" href="/docs/features/bot-session-compaction">
    Summarise older bot turns instead of dropping them
  </Card>
</CardGroup>
