Session Persistence
PraisonAI Agents provides automatic session persistence with zero configuration. Simply provide asession_id to your Agent and conversation history is automatically saved and restored.
Released in PR #1897 — Earlier versions wrote duplicate messages when
memory="history" was combined with auto_save (one turn produced 4 messages instead of 2), and Session.save_state() re-appended full history on every call. Both paths are now exact and idempotent.Released in PR #1709 — Earlier versions could silently drop the latest messages when
update_session_metadata() ran concurrently with add_message() across processes or store instances. Upgrade to pick up the fix.Released in PR #2102 — Earlier versions of the gateway session-resume path used the first
session_data system message it found, which was the empty snapshot written at session create. Reconnecting always restored an empty history. Resume now iterates all system messages and keeps the latest snapshot. Also: WebSocketGateway.stop() previously called session.close() directly, bypassing persistence; it now goes through close_session(), so graceful shutdown (SIGTERM) persists in-flight sessions identically to a WebSocket disconnect.Released in PR #1972 — Earlier versions of
BotSessionManager keyed agent locks on id(agent), which CPython is free to reuse once an agent is garbage-collected. In long-running gateways that recreated agents per request, two unrelated users could silently serialize on the same lock — or worse, swap histories. Agent locks now use a WeakKeyDictionary, so they auto-clean when the agent is GC’d. Per-user locks are bounded by an LRU+TTL cache. Upgrade to pick up the fix.Quick Start
How It Works
When you provide asession_id to an Agent:
- Automatic Persistence: Conversation history is automatically saved to disk after each message
- Automatic Restoration: When a new Agent is created with the same
session_id, history is restored - 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
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.| 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 withoutsession_id, the same Agent instance remembers previous messages:
In-memory memory is lost when the Agent instance is garbage collected or the process ends.
Use
session_id for persistence across processes.Persistent Sessions
Basic Usage
Resuming Sessions
Session File Format
Sessions are stored as JSON files with automatic metadata tracking: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 |
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:
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.
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 insideFileLock, mutates, then atomically writes (temp file +os.replace). Concurrent writers cannot drop each other’s messages. - Fresh reads —
get_chat_history,get_session, andget_sessions_by_agentreload from disk underFileLockon every call and refresh the in-process cache. Two store instances pointing at the samesession_dirwill always see each other’s writes. - Cross-platform locks —
fcntl.flockon Unix/macOS,msvcrt.lockingon Windows.
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. UnifiedSessionStore now reloads and merges under exclusive lock — shared message-prefix merge + delta-based stats merge — as of PR #1885. Updated in PR #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 for the CLI-side details.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.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.Direct Session Store Access
For advanced use cases, you can access the session store directly:Custom Session Directory
Using with DB Adapter
When a DB adapter is provided, it takes precedence over JSON persistence. TheDbSessionAdapter now persists both messages and metadata to the conversation store, ensuring session metadata survives process restarts.
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.Context Caching
For cost optimization with Anthropic models, usecaching=True:
Bot Session Persistence
Bots use the same session store as agents. Each user gets a persistent session that survives bot restarts. Configure viabot.yaml:
max_history Resolution
When a bot starts, it resolvesmax_history using this precedence ladder:
Bot Session Configuration
Configure these settings in yourbot.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). |
max_history at the channel level takes precedence over session.max_history. Use session.max_history for new configs — it is the preferred form.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. |
Without a
store parameter, BotSessionManager falls back to in-memory-only mode for backward compatibility.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 |
id(agent) keys across unrelated users.
Session Store Protocol
All session stores implementSessionStoreProtocol — a lightweight interface that enables swapping backends:
Learn more about building custom session stores
Best Practices
-
Use meaningful session IDs: Include user ID or context in the session ID
- Handle session limits: Default max messages is 100. Older messages are trimmed.
-
Clean up old sessions: Use
store.delete_session()to remove unused sessions, which also purges persisted DB rows. -
Use prompt caching: Enable
caching=Truefor Anthropic models to reduce costs.
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) |

