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

> Default JSON storage, hierarchical sessions, identity, and task-local context

Session stores persist chat history and metadata — swap the default JSON backend or use hierarchical forks without changing your agent code.

<Warning>
  `SqliteSessionStore` (this page) and `SqliteTranscriptStore` (see [SQLite Transcript Store](/docs/features/sqlite-transcript-store)) are **two different types**. `SqliteSessionStore` adds an **FTS5 search index** for scalable cross-session recall; `SqliteTranscriptStore` replaces **persistence** with WAL SQLite for gateway concurrency and is the **default** gateway backend as of PraisonAI PR #3409. Choose one — running both against the same DB file is not tested.
</Warning>

<Note>
  As of PraisonAI PR #3637, `AgentTeam.save_session_state` / `restore_session_state` also route through this store (via `update_session_metadata` / `get_session`), so team resume is now memory-independent.
</Note>

<Note>
  As of PraisonAI PR #3648, `DefaultSessionStore` also accepts a `mirror=` sink implementing [`SessionMirrorProtocol`](/docs/features/session-mirror) — every persisted record is additionally replicated to the mirror on a background thread, local-first (a mirror outage never blocks the turn). Default `mirror=None` keeps the store byte-for-byte unchanged.
</Note>

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

agent = Agent(
    name="Assistant",
    memory={"session_id": "user-42-chat"},
)
agent.start("Remember I like tea.")
agent.start("What do I like?")  # History restored from ~/.praisonai/sessions/
```

The user chats across restarts; the session store persists history under `~/.praisonai/sessions/`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[Agent] --> P[SessionStoreProtocol]
    P --> D[DefaultSessionStore]
    P --> H[HierarchicalSessionStore]
    P --> S[SqliteSessionStore]

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

    class A agent
    class P,D,H,S store
```

## How It Works

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

    User->>Agent: Request
    Agent->>Feature: Process request
    Feature-->>Agent: Result    Agent-->>User: Response
```

## Where sessions are stored

The session directory is resolved live from `PRAISONAI_HOME`, so relocating the store root works even after PraisonAI is imported (PraisonAI PR #4125).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Arg[session_dir arg] --> R{Resolve dir}
    Attr[DEFAULT_SESSION_DIR override] --> R
    Env[PRAISONAI_HOME] --> R
    Def[~/.praisonai/sessions] --> R
    R --> Store[(Session Store)]

    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 Arg,Attr,Env,Def input
    class R process
    class Store output
```

Resolution order (highest priority first):

| Source | Wins when | Result |
| - | - | - |
| `session_dir=` constructor arg | Passed to `DefaultSessionStore(...)` | That exact directory |
| `praisonaiagents.session.store.DEFAULT_SESSION_DIR = "..."` | Explicitly assigned | Pins the root process-wide (test/embedding hook) |
| `PRAISONAI_HOME` env var | Set (no explicit override above) | `$PRAISONAI_HOME/sessions/` — resolved live |
| (default) | Nothing set | `~/.praisonai/sessions/` |

Set `PRAISONAI_HOME` before the first turn to redirect storage — it is honoured live, even after `praisonaiagents` is imported:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import os
os.environ["PRAISONAI_HOME"] = "/data/praisonai"   # honoured live, even after import

from praisonaiagents import Agent

agent = Agent(name="Assistant", memory={"session_id": "user-42-chat"})
agent.start("Remember I like tea.")   # saved under /data/praisonai/sessions/
```

<Note>
  `get_default_session_store()` tracks the resolved directory and **rebuilds** the process-wide singleton when that directory changes — so a multi-tenant host that switches `PRAISONAI_HOME` at runtime gets the correct per-tenant directory. A store you **inject** (`praisonaiagents.session.store._default_store = my_store`) is always honoured verbatim and never rebuilt.
</Note>

<Warning>
  This is a fix relative to older releases. Before PraisonAI PR #4125, `PRAISONAI_HOME` set **after** import was silently ignored and the store could read, write, and delete sessions in the wrong directory. Set `PRAISONAI_HOME` at process start for deterministic behaviour.
</Warning>

<Note>
  `DEFAULT_SESSION_DIR` remains a readable, assignable module attribute — but is now resolved at access time. Reading it returns the live directory; assigning it (`store_module.DEFAULT_SESSION_DIR = "/some/path"`) pins the root process-wide.
</Note>

## Portable export/import

`DefaultSessionStore`, `SqliteSessionStore`, and `SqliteTranscriptStore` all implement `PortableSessionStoreProtocol`, so you can back up, migrate, or restore live conversation state as a versioned JSON payload.

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

store = DefaultSessionStore()
payload = store.export_all()               # {"version": 1, "sessions": [...]}
report  = store.import_sessions(payload)   # ImportReport (imported / skipped)
```

* Portability is **protocol-level**, not store-level — any store that implements `PortableSessionStoreProtocol` gets the same backup/restore surface.
* Import resets live routing fields (`gateway_session_id`, `agent_id`) by default, so restored sessions stay inert until re-bound.

See [Gateway Session Portability](/docs/features/gateway-session-portability) for the operator-facing backup / migrate / restore story.

<Note>
  **`DefaultSessionStore`, `SqliteSessionStore`, and `EncryptedSessionStore` support incremental streamed-turn persistence** (PraisonAI PR [#5413](https://github.com/MervinPraison/PraisonAI/pull/5413) / issue [#5407](https://github.com/MervinPraison/PraisonAI/issues/5407)). Interrupted streamed runs resume with their partial assistant turn via `praisonai run --continue` / `--session <id>`. See [Incremental Session Store Protocol](/docs/features/session-protocol#incremental-session-store-protocol).
</Note>

## Quick Start

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

    agent = Agent(
        name="Assistant",
        memory={"session_id": "user-42-chat"},
    )
    agent.start("Remember I like tea.")
    agent.start("What do I like?")
    ```

    Default files live at `~/.praisonai/sessions/{session_id}.json`.
  </Step>

  <Step title="Use the store directly">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents.session import get_default_session_store

    store = get_default_session_store()
    session_id = "user-42-chat"

    agent = Agent(name="Assistant", memory={"session_id": session_id})
    store.add_message(session_id, "assistant", agent.start("Summarise our chat"))
    ```
  </Step>
</Steps>

<Note>
  **Sub-agent transcripts live inside the parent session's record** (PraisonAI PR #4126). Transcripts from `session.Agent(name, role=...)` are persisted under `metadata["agent_histories"][agent_key]` on the parent session — not as separate top-level sessions. You don't need to do anything: they round-trip automatically with the parent.

  ```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  graph TB
      subgraph SD["SessionData (session_id='chat')"]
          M["metadata.agent_histories"]
          M --> K1["support:agent → [ ...messages... ]"]
      end

      classDef session fill:#8B0000,stroke:#7C90A0,color:#fff
      classDef meta fill:#189AB4,stroke:#7C90A0,color:#fff
      classDef msgs fill:#10B981,stroke:#7C90A0,color:#fff

      class SD session
      class M meta
      class K1 msgs
  ```

  Restore reads, in order:

  1. The parent record's `agent_histories` map (always wins on key collisions).
  2. Tagged legacy per-agent records (`parent_session_id` + `agent_key`) — migrated forward into the parent's `agent_histories` on the next save. Migration scans **every** stored session, not just recent ones, so a tagged record is always picked up no matter how many newer sessions sit above it (PraisonAI PR [#4162](https://github.com/MervinPraison/PraisonAI/pull/4162) — before this, records outside the default 50-session recency window were silently skipped).
  3. Untagged pre-tag records — **only** when `PRAISONAI_SESSION_LEGACY_AGENT_KEYS=1` is set (accepts `1` / `true` / `yes` / `on`, case-insensitive; default off). Otherwise they are ignored — a warning names the exact record — because their sanitised id `"{parent}_{agent_key}"` is indistinguishable from a real user session of that name.

  Legacy records are **never deleted**. Concurrent saves under the same parent are safe (bounded read-verify-write, up to 5 retries). `list_sessions()` still surfaces `agent_key` / `parent_session_id`, now used for legacy-record discovery. The full-store scan runs at most **once per `Session` instance** — the result is memoised, keyed on `PRAISONAI_SESSION_LEGACY_AGENT_KEYS`, so restoring N sub-agents costs one scan, not N.
</Note>

<Warning>
  Set `PRAISONAI_SESSION_LEGACY_AGENT_KEYS=1` only when you know which records on disk are legacy sub-agent transcripts. An untagged id like `"chat_support_agent"` may in fact be a real user session of that sanitised name.
</Warning>

## Encryption at rest

Wrap any store — `DefaultSessionStore`, `SqliteSessionStore`, `HierarchicalSessionStore`, or a custom backend — with `EncryptedSessionStore` so message content and metadata are encrypted before they reach disk.

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

store = EncryptedSessionStore(
    SqliteSessionStore(db_path="~/.praisonai/sessions.db"),
    key=os.environ["PRAISONAI_SESSION_KEY"],
)
```

See [Encrypted Session Store](/docs/features/encrypted-session-store) for keys, honest limits, and migration.

## Core Exports

| Export | Purpose |
| - | - |
| `DefaultSessionStore` | JSON-on-disk default backend |
| `SqliteSessionStore` | Indexed variant of the default store — stdlib `sqlite3` + FTS5 for scalable cross-session recall (Issue #2927). See [SqliteSessionStore](#sqlitesessionstore). |
| `EncryptedSessionStore` | Wrap any session store to encrypt message content and metadata at rest — see [Encrypted Session Store](/docs/features/encrypted-session-store) |
| `SessionEncryptionError` | Raised on missing/invalid keys, wrong-key decryption, or `search()` calls on an encrypted store |
| `generate_session_key()` | Generate a Fernet key suitable for `PRAISONAI_SESSION_KEY` |
| `SessionMirrorProtocol` | Contract for a local-first remote sink — see [Session Mirror](/docs/features/session-mirror) |
| `SessionMessage`, `SessionData` | Typed message and session payloads |
| `CompactionCheckpoint` | Persisted compaction summary + resume anchor — see [Compaction Checkpoints](#compaction-checkpoints) |
| `get_default_session_store()` | Process-wide store accessor |
| `SessionStoreProtocol` | Implement for Redis, Postgres, S3 — see [Session Protocol](/docs/features/session-protocol) |
| `HierarchicalSessionStore`, `get_hierarchical_session_store()` | Forks, snapshots, parent-child — see [Session Hierarchy](/docs/features/session-hierarchy) |
| `IdentityResolverProtocol`, `FileIdentityResolver` | Map anonymous → known user IDs across sessions |
| `SessionContext`, `set_session_context()`, `get_session_context()` | Task-local session context for async flows |

### `DefaultSessionStore` constructor

| Parameter | Type | Default | Description |
| - | - | - | - |
| `mirror` | `Optional[SessionMirrorProtocol]` | `None` | Optional remote sink for local-first mirroring. See [Session Mirror](/docs/features/session-mirror). |

## SqliteSessionStore

`SqliteSessionStore` is a drop-in subclass of `DefaultSessionStore` that keeps JSON transcripts as the durable record and maintains a stdlib `sqlite3` index alongside them. It gives you two indexed hot paths instead of directory scans:

* **Cross-session search** — FTS5 index of message content, used by `search()` (Issue #2927).
* **Gateway/agent routing** — `session_route` index of `gateway_session_id` and `agent_id`, used by `get_by_gateway_session()` and `list_sessions_by_gateway_agent()` (Issue #2956).

Both stay independent of the number of stored sessions, so a long-lived gateway bot with thousands of sessions still routes an inbound message in a single indexed lookup.

`SqliteSessionStore` opens its underlying FTS5 index through `sqlite_connect(..., guard=True)`, so a malformed index file is detected on open, forensically backed up, and repaired in place instead of crashing the store. See [Hardened SQLite Storage → Integrity Guard](/docs/features/sqlite-hardened-connector#integrity-guard).

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

store = SqliteSessionStore(db_path="~/.praisonai/sessions.db")
agent = Agent(name="Gateway", session_store=store)
```

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

store = SqliteSessionStore(db_path="~/.praisonai/sessions.db")

# Long-lived gateway bot: inbound message arrives tagged with a gateway_session_id.
# Routing back to the right session is an indexed lookup, not a directory scan.
agent = Agent(name="Gateway", session_store=store)
agent.start("Handle inbound message", session_id="chat-42")

# Later — resolve the local session for an inbound gateway event:
session = store.get_by_gateway_session("gw-abc-123")
recent = store.list_sessions_by_gateway_agent("agent-support", limit=20)
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "SqliteSessionStore"
        W[📝 Write] --> M[(session_meta<br/>+ session_fts)]
        W --> R[(session_route)]
        S[🔍 search] --> M
        G[🌐 gateway lookup] --> R
        A[🤖 agent lookup] --> R
    end

    classDef write fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef store fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef read fill:#10B981,stroke:#7C90A0,color:#fff

    class W write
    class M,R store
    class S,G,A read
```

### Constructor

| Parameter | Type | Default | Description |
| - | - | - | - |
| `session_dir` | `Optional[str]` | `None` | Directory for session JSON files (as in `DefaultSessionStore`). |
| `db_path` | `Optional[str]` | `<session_dir>/sessions_index.db` | Path to the SQLite index file. Use `":memory:"` for an ephemeral in-process index. Supports `~` expansion. |
| `**kwargs` | — | — | Forwarded to `DefaultSessionStore(...)`. |

### Behaviour

| Aspect | Detail |
| - | - |
| Backend | Stdlib `sqlite3` (lazy-imported). Content index: FTS5 virtual table `session_fts(session_id UNINDEXED, content)` + `session_meta(session_id, updated_at)`. Routing index: `session_route(session_id PRIMARY KEY, gateway_session_id, agent_id)` with `idx_route_gateway` and `idx_route_agent`. |
| Search query | `SELECT session_id FROM session_fts WHERE session_fts MATCH ? ORDER BY bm25(session_fts) LIMIT ?` |
| Gateway lookup | `SELECT session_id FROM session_route WHERE gateway_session_id = ?` (overrides parent's O(N) directory scan) |
| Agent lookup | `SELECT session_id FROM session_route WHERE agent_id = ? LIMIT ?` (overrides parent's O(N) directory scan) |
| Fallback | If FTS5 is unavailable → plain `LIKE` table. If the routing query fails or `sqlite3` is unavailable → transparent fallback to `DefaultSessionStore.get_by_gateway_session` / `list_sessions_by_gateway_agent` scan. |
| Backfill | Legacy JSON transcripts are indexed once, on first `search()` **or** first routing lookup. A session is treated as indexed only if it appears in **both** `session_meta` **and** `session_route`, so stores upgraded from a prior release re-populate route rows on first read. |
| Indexed content | `_flatten` concatenates `archived_messages` (compacted turns) ahead of the active `messages`, so compacted history stays searchable (Issue #5031). |
| Content version | `INDEX_CONTENT_VERSION = 2`. Persisted in the `session_index_meta` table (`content_version` key). A stale version forces a one-time FTS rebuild on startup. |
| Write sync | `_save_session`, `add_message`, and `_modify_session_locked` (covers `set_chat_history`, `set_gateway_info`, `append_compaction_checkpoint`, and `clear_session`) all re-index the retained content and routes. `delete_session` is the only operation that deindexes a session (removes rows from `session_meta`, `session_fts`, and `session_route`). Route rows are `INSERT OR REPLACE`d when either `gateway_session_id` or `agent_id` is set, and `DELETE`d (via `delete_session`) when the session is removed. |
| Query rewrite | Free text is tokenised on alphanumerics into an `OR` of quoted terms for a safe FTS5 `MATCH` expression. |
| Candidate fan-out | Over-fetches `max(limit * 5, limit)` candidates so lineage dedup / automated demotion can still promote the right sessions into the final `limit`. |
| Corrupt-transcript tolerance | `search()` skips unreadable candidates — `OSError`, `JSONDecodeError`, and `UnicodeDecodeError` — on both the FTS5 and `LIKE` candidate paths, so one bad transcript on disk cannot abort recall across the remaining valid sessions. See [Cross-Session Recall → Resilience to Corrupt Transcripts](/docs/features/cross-session-recall#resilience-to-corrupt-transcripts). |
| Malformed-turn tolerance | `SessionData.from_dict` skips any entry in `messages` or `archived_messages` that is not a JSON object (`null`, a string, a number). This covers the SQLite indexed route, the `SqliteTranscriptStore` indexed route, and the default-store scan fallback — a single malformed turn on disk no longer crashes `get_by_gateway_session` or `list_sessions_by_gateway_agent`. The on-disk transcript and its route index record are left untouched. When one or more dropped active turns precede the compaction anchor, `last_compaction.message_index` is shifted down by the number of entries dropped before it so `get_working_history()` still slices the retained tail correctly. See PraisonAI PR [#5676](https://github.com/MervinPraison/PraisonAI/pull/5676) / issue [#5672](https://github.com/MervinPraison/PraisonAI/issues/5672). |

<Note>
  **`clear_session` preserves routing and archive on `SqliteSessionStore`** (parity with `DefaultSessionStore`, PraisonAI PR [#5681](https://github.com/MervinPraison/PraisonAI/pull/5681) / issue [#5521](https://github.com/MervinPraison/PraisonAI/issues/5521)). Clearing a session empties only the active turns — the JSON transcript, `archived_messages`, and gateway/agent bindings all remain on disk, and the matching rows in `session_meta`, `session_fts`, and `session_route` are retained. `get_by_gateway_session`, `list_sessions_by_gateway_agent`, and archived-only `search` continue to return the session as an indexed lookup. Only `delete_session` deindexes a session.
</Note>

A long-lived gateway bot can reset the active window without losing its route:

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

store = SqliteSessionStore(db_path="~/.praisonai/sessions.db")
agent = Agent(name="Gateway", session_store=store)

# Session is bound to a gateway event and handles a conversation
agent.start("Hello", session_id="chat-42")
store.set_gateway_info("chat-42", gateway_session_id="gw-abc", agent_id="support")

# Operator clears the active window to start a fresh chat for the same user
store.clear_session("chat-42")

# The next inbound gateway event still routes to the same local session —
# the route index survives clear_session (PR #5681).
same = store.get_by_gateway_session("gw-abc")   # SessionData for "chat-42"
assert same.session_id == "chat-42"
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "clear_session on SqliteSessionStore"
        C[🧹 clear_session] --> A[Active turns emptied]
        C --> K1[📦 archived_messages retained]
        C --> K2[🌐 gateway_session_id retained]
        C --> K3[🤖 agent_id retained]
        C --> K4[🔎 FTS + route indexes retained]
    end
    D[🗑️ delete_session] --> R[All of the above removed]

    classDef op fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef kept fill:#10B981,stroke:#7C90A0,color:#fff
    classDef gone fill:#F59E0B,stroke:#7C90A0,color:#fff

    class C,D op
    class A,K1,K2,K3,K4 kept
    class R gone
```

### Fallback matrix

| Environment | Behaviour |
| - | - |
| `sqlite3` + FTS5 present (default) | Bounded FTS5 `MATCH` lookup for search, indexed `SELECT` on `session_route` for gateway/agent routing |
| `sqlite3` present, FTS5 unavailable | Bounded `LIKE` lookup for search; routing index still active |
| `sqlite3` unavailable or index open fails | Transparent fallback to `DefaultSessionStore.search` and `DefaultSessionStore.get_by_gateway_session` / `list_sessions_by_gateway_agent` full-directory scans |

### Sizing

* Each row in `session_fts` holds the flattened concatenation of a session's message content (newline-joined). Large transcripts increase index size roughly linearly.
* Use `":memory:"` for ephemeral tests; use the default disk path for gateway bots that need durability across restarts.

Bookends, automated demotion, and lineage dedup apply to results from both stores — see [Cross-Session Recall](/docs/docs/features/cross-session-recall#anchored-demoted-deduped-results).

### Upgrade note: archived-history reindex

<Note>
  As of Issue #5031, `SqliteSessionStore` also indexes `archived_messages` (turns compacted out of the active window), bumping `INDEX_CONTENT_VERSION` to `2`. On the **first startup after upgrade**, the store detects the stale version and rebuilds every session's FTS row once so archived-only queries work immediately — rather than only after each session's next write. No user action is required: startup runs a bounded backfill, then persists the new version so later startups fall back to the cheap skip-already-indexed path. A new internal `session_index_meta` key/value table (`content_version` key) tracks the version, so operators upgrading an existing store should expect that table to appear in the index file.
</Note>

See [Cross-Session Recall → Recall Across Compacted History](/docs/features/cross-session-recall#recall-across-compacted-history) for the caller-facing behaviour.

## Compaction Checkpoints

When context compaction runs during a conversation, the store can persist the summary so a later resume replays the compacted working history (summary + retained tail) instead of the full raw transcript. See [Compacted Session Resume](/docs/features/session-compaction-checkpoint) for the end-to-end agent flow.

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

store = DefaultSessionStore()
store.append_compaction_checkpoint("chat-42", "Earlier: we discussed X, Y, Z.")
history = store.get_working_history("chat-42")   # summary + tail
```

### Store Methods

| Method | Description |
| - | - |
| `append_compaction_checkpoint(session_id, summary, *, role="system", tokens_before=0, tokens_after=0, metadata=None)` | Persist a checkpoint anchored to the current end of the transcript. Returns `bool`; a blank/whitespace summary is a no-op returning `False`. |
| `get_working_history(session_id, max_messages=None)` | Canonical read path — uses the checkpoint when present (summary + tail), falls back to raw chat history when not. |

### SessionData additions

`SessionData.last_compaction` holds the latest `CompactionCheckpoint` (or `None`). Two helpers support cheap resume:

| Member | Description |
| - | - |
| `last_compaction` | `Optional[CompactionCheckpoint]` — the persisted checkpoint, serialised into the session JSON |
| `trim_messages(max_messages)` | Trim the transcript head, shifting the checkpoint anchor so the retained tail stays aligned |
| `get_working_history(max_messages=None)` | Reconstruct `[summary_message, *tail]`; falls back to `get_chat_history` with no checkpoint |

<Note>
  `set_chat_history()` and `clear_session()` both clear `last_compaction` — replacing or clearing the active transcript invalidates the anchor. Raw turns already moved into `archived_messages` by prior compactions are **not** removed, and stay searchable through `search()` and `session_search` (PraisonAI PR [#5681](https://github.com/MervinPraison/PraisonAI/pull/5681)).
</Note>

## Task-Local Context

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

set_session_context(session_id="batch-job-1", user_id="operator")

ctx = get_session_context()
print(ctx.session_id)
```

## Best Practices

<AccordionGroup>
  <Accordion title="Prefer session_id on Agent over manual store calls">
    Let `Agent(memory={"session_id": "..."})` handle persistence — use the store directly only for admin, migration, or custom backends.
  </Accordion>

  <Accordion title="Use hierarchical store for forks and snapshots">
    Switch to `get_hierarchical_session_store()` when you need branching conversations or revert — see [Session Hierarchy](/docs/features/session-hierarchy).
  </Accordion>

  <Accordion title="Set task-local context in async workers">
    Call `set_session_context()` at the start of each async task so downstream code reads the correct session without threading IDs through every call.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Session Persistence" icon="floppy-disk" href="/docs/features/session-persistence">
    Agent-centric session\_id usage
  </Card>

  <Card title="Session Hierarchy" icon="sitemap" href="/docs/features/session-hierarchy">
    Forking and snapshots
  </Card>

  <Card title="Cross-Session Recall" icon="magnifying-glass-clock" href="/docs/features/cross-session-recall">
    Search past sessions — anchored, demoted, deduped results
  </Card>

  <Card title="Encrypted Session Store" icon="lock" href="/docs/features/encrypted-session-store">
    Encrypt transcripts at rest by wrapping any store
  </Card>
</CardGroup>


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