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

<Note>
  The SQLite transcript default introduced in PraisonAI PR #3409 is **gateway-only**. The single-user code paths on this page are unaffected — a plain `Agent` script keeps using the default JSON store. See [SQLite Transcript Store](/docs/features/sqlite-transcript-store) for the gateway change.
</Note>

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

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")
```

<Warning>
  `session_id` belongs inside `memory=`, not on `Agent` directly.

  ❌ `Agent(name="Assistant", session_id="my-session")` → `TypeError`
  ✅ `Agent(name="Assistant", memory=MemoryConfig(session_id="my-session"))`

  The same applies to `db=` and `user_id=` — all three are memory config options.
</Warning>

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>

<Note>
  **Works for AgentTeam / YAML runs too.** `praisonai run agents.yaml --continue` / `--session <id>` resumes a whole team — per-agent chat history and shared team state replay before the new prompt. See [YAML / Team Session Continuity](/docs/features/yaml-session-continuity).
</Note>

## Quick Start

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

    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, MemoryConfig

# 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

<Note>
  Assistant tool-call turns and `role="tool"` result turns are round-tripped by the default JSON store — a resumed model sees the same tool history it produced before. File-format compatibility is preserved: old text-only files load unchanged. See [Session Resume](/docs/persistence/session-resume) for the full contract.
</Note>

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/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`

<Note>
  This path honours `PRAISONAI_HOME` and is resolved **live** — setting `PRAISONAI_HOME` before the first agent turn (even after importing `praisonaiagents`) redirects session storage to `$PRAISONAI_HOME/sessions/`. See [Session Store → Where sessions are stored](/docs/features/session-store#where-sessions-are-stored) for the full resolution order.
</Note>

## 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](/docs/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, MemoryConfig

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, MemoryConfig

# 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, MemoryConfig

# 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                                                                                                                                                                                                                       |
| `agent_histories` | `dict`   | Populated by `Session._save_agent_chat_histories()` when `session.Agent(name, role=...)` sub-agents exist — maps `agent_key → [messages]` (PraisonAI PR #4126). See [Sessions → Multi-Agent Sessions](/docs/features/sessions#multi-agent-sessions). |

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/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>

## Write-failure salvage

When a durable session write fails, the turn is salvaged to a spill file and re-folded on the next load — nothing is silently lost.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Store as SessionStore
    participant Disk as Session file
    participant Spill as ~/.praisonai/state/session_spill/
    participant Hook as SESSION_PERSIST_FAILED

    Agent->>Store: add_message(...)
    Store->>Disk: atomic write
    alt Write OK
        Disk-->>Store: True
        Store-->>Agent: True
    else Write fails (disk-full / OSError / corruption)
        Disk-->>Store: False
        Store->>Spill: atomic spill (0600, unique filename)
        Store->>Hook: fire (role, content, error, spilled, spill_path)
        Store-->>Agent: False
    end

    Note over Store,Spill: On next load: re-ingest spills,<br/>dedup on (role, content, timestamp),<br/>run retention window, then delete files.
```

On a durable-write failure the store spills **just that turn** to an atomic fallback file under `~/.praisonai/state/session_spill/`, then fires the observe-only [`SESSION_PERSIST_FAILED`](/docs/features/hook-events#session-persist-failed-a-durable-write-failed) hook so operators can alert on the failure. On the next session **load** the store scans the spill directory for this session's files and re-ingests any un-persisted turns.

| Aspect           | Value                                                                                       |
| ---------------- | ------------------------------------------------------------------------------------------- |
| Spill directory  | `~/.praisonai/state/session_spill/`                                                         |
| Filename shape   | `<safe_session_id>.<ms>.<pid>.<random4hex>.json`                                            |
| File permissions | `0o600` (owner read/write only)                                                             |
| Payload          | `{session_id, spilled_at, messages: [SessionMessage.to_dict(), …]}`                         |
| Recovery         | On next `load`, re-ingested, deduped on `(role, content, timestamp)`, retention re-enforced |
| Cleanup          | Spill file deleted only after a successful re-insert                                        |
| Malformed spill  | Skipped; does not block recovery of other spills                                            |
| Happy path       | Nothing is written to the spill directory                                                   |

Key guarantees, all from `store.py`:

* **Atomic, private writes** — temp file + `os.replace` + best-effort dir `fsync`, and the final file is chmod'd to `0o600`.
* **No lost turns on rapid failure** — the random 4-hex suffix means consecutive same-ms/PID failures never overwrite each other.
* **No duplicates on recovery** — re-ingest is de-duplicated on `(role, content, timestamp)`, so an already-persisted turn is never doubled.
* **Retention still applies** — recovered turns run through `_enforce_window` before write-back, so recovery respects `max_messages` / `active_window` / `retention` like any ordinary write.
* **One bad file can't block the rest** — malformed spills (non-object root / non-list `messages` / non-object message) are skipped, not fatal.
* **Retry-safe** — spill files are deleted only after a successful re-insert; if the re-ingest write itself fails, spills stay in place for the next load to retry.

No new `Agent` params, no new dependencies, and no behaviour change on the happy path — subscribe to the hook to observe the failure:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, MemoryConfig
from praisonaiagents.hooks.registry import get_default_registry
from praisonaiagents.hooks.types import HookEvent, HookResult

registry = get_default_registry()

@registry.on(HookEvent.SESSION_PERSIST_FAILED)
def on_persist_failed(event_data):
    print(
        f"[persist-failed] session={event_data.session_id} "
        f"role={event_data.role} spilled={event_data.spilled} "
        f"path={event_data.spill_path} error={event_data.error}"
    )
    return HookResult.allow()

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")
```

## Corruption-quarantine on load

If a session `.json` is unreadable at load time (malformed JSON or invalid UTF-8), the store moves it aside to `<file>.json.corrupt-<epoch_ms>` before starting a fresh session — the raw bytes stay recoverable instead of being silently overwritten by the next write.

Before this fix, the next write clobbered the corrupt file with an empty session and the user's entire conversation history was gone with only a log line. Now nothing is destroyed: a corrupt copy is always preserved, and the event is observable — not just logged.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    L[💾 Load session] --> D{🔍 JSON / UTF-8 decode ok?}
    D -->|Yes| OK[✅ Return session]
    D -->|No| Q[📦 Quarantine to .json.corrupt-&lt;ts&gt;]
    Q --> H[🪝 Fire SESSION_PERSIST_FAILED]
    H --> F[🟢 Return fresh SessionData]

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef quarantine fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class L input
    class D,H process
    class Q quarantine
    class OK,F result
```

### What triggers it

A `json.JSONDecodeError` or a `UnicodeDecodeError` on the on-disk session file quarantines the bytes and starts fresh. The same contract applies to both `DefaultSessionStore` and the `HierarchicalSessionStore` (`ExtendedSessionData`) override — a corrupt binary file is quarantined in either store rather than propagating.

<Note>
  A transient `OSError` on read (permission errors, NFS/network blips, antivirus locks, disk-full-during-read) is **re-raised**, not quarantined. The caller aborts the pending write, so a healthy on-disk copy is never destroyed by a flaky read. `json.JSONDecodeError` is a subclass of `ValueError`, not `OSError`, so genuine corruption never falls into this branch.
</Note>

### Where the quarantine lands

The quarantined file sits next to the original in the same session directory (whatever store is configured — default `~/.praisonai/sessions/…`).

| Aspect           | Value                                                                                                           |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| Filename shape   | `<original>.json.corrupt-<epoch_ms>`                                                                            |
| Collision suffix | `-1`, `-2`, … appended when two corruptions land in the same millisecond, so every copy is preserved distinctly |
| Rename           | `os.replace(filepath, quarantine_path)` under the session `FileLock` — writes cannot race the quarantine        |
| Best-effort      | If the rename itself fails, the quarantine path is `None` and the hook still fires                              |
| Triggers         | `json.JSONDecodeError` **or** `UnicodeDecodeError`                                                              |
| Does not trigger | `OSError` on read — re-raised so the caller aborts the write                                                    |

### How to observe it

Subscribe to `SESSION_PERSIST_FAILED`; the corruption branch reuses the existing payload with distinguishing semantics.

| Field                            | On corruption                                                                |
| -------------------------------- | ---------------------------------------------------------------------------- |
| `session_id`                     | The affected session id                                                      |
| `error`                          | Begins with `"corrupt session file: "` followed by the decoder error text    |
| `spilled`                        | Repurposed to carry the **quarantine path** (or `None` if the rename failed) |
| `role`, `content`                | Empty strings — there is no in-flight turn to attach                         |
| `event_name`, `timestamp`, `cwd` | As usual                                                                     |

The `"corrupt session file:"` prefix on `error` distinguishes a corrupt read from the older write-failure branch, which uses different `error` text and carries a spill path in `spilled`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, MemoryConfig
from praisonaiagents.hooks import add_hook, HookEvent

# Alert on corrupt session files so a silent history-loss event
# becomes an actionable notification (no code changes needed
# for the quarantine itself — it is on by default).
@add_hook(HookEvent.SESSION_PERSIST_FAILED)
def on_session_corruption(inp):
    if inp.error.startswith("corrupt session file:"):
        print(f"Session {inp.session_id} was corrupt; quarantined to {inp.spilled}")

agent = Agent(name="Assistant", instructions="Answer the user.")
agent.start("Hi")
```

### How to recover manually

Inspect the `.json.corrupt-<ts>` file with any JSON tool, hex editor, or `jq`, hand-repair the malformed segment (or extract salvageable turns), then either drop the repaired content back into place at `<file>.json` or import it via the existing session-import path.

<Tip>
  Corruption-quarantine and [Write-failure salvage](#write-failure-salvage) are two branches of the same "no silent data loss" contract: a bad *write* spills the turn and re-ingests it later; a bad *read* quarantines the file and preserves it for recovery. Both fire `SESSION_PERSIST_FAILED` so operators can alert on either.
</Tip>

## 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, MemoryConfig

# 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=MemoryConfig(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

Prompt caching is opt-in via `caching=CachingConfig(prompt_caching=True)`:

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

agent = Agent(
    name="Assistant",
    memory=MemoryConfig(session_id="my-session"),
    caching=CachingConfig(prompt_caching=True),
)
```

On Anthropic models this injects `cache_control` breakpoints on the system block and stable history prefix. OpenAI and Gemini cache the prefix automatically when it is byte-stable, so no wire change is needed there.

## 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, MemoryConfig
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](/docs/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, MemoryConfig

# 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

The global default store is a lazy singleton keyed on the **resolved session directory**. Changing `PRAISONAI_HOME` mid-process changes that directory, which **rebuilds** the singleton and **re-reads** these env vars as part of constructing the new store (PraisonAI PR #4125). Changing only the retention/active-window vars — with **no** directory change — has no effect on an already-built store; those values stay pinned to first construction. Set all three at process start for deterministic behaviour.

| 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(
            memory=MemoryConfig(
                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

<Note>
  When you use `SqliteSessionStore` instead of the default JSON store, `get_by_gateway_session()` and `list_sessions_by_gateway_agent()` become indexed lookups against the `session_route` table (Issue #2956) — routing latency stays flat as sessions accumulate. See [Session Store → SqliteSessionStore](/docs/docs/features/session-store#sqlitesessionstore).
</Note>

| 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                                                 |
| `list_sessions_by_gateway_agent(agent_id, limit)`            | List session ids linked to a gateway agent 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>

  <Card title="Hook Events" icon="webhook" href="/docs/features/hook-events#session-persist-failed-a-durable-write-failed">
    Subscribe to `SESSION_PERSIST_FAILED` to alert on write failures and corrupt reads
  </Card>
</CardGroup>
