Skip to main content
Provide a session_id on your agent and conversation history is saved and restored automatically — no database setup required.
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 for the gateway change.
session_id belongs inside memory=, not on Agent directly.Agent(name="Assistant", session_id="my-session")TypeErrorAgent(name="Assistant", memory=MemoryConfig(session_id="my-session"))The same applies to db= and user_id= — all three are memory config options.
The user returns with the same session_id; prior turns reload automatically from disk.
Upgrade if you rely on multi-process saves, gateway resume, or idempotent save_state() — fixes landed in PRs #1709, #1897, #1972, and #2102.
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.

Quick Start

1

Start with a session_id

2

Resume from the CLI

History, model, and agent name are restored — not just the transcript.

Agent Session Persistence

Deterministic Resume

As of PR #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
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 for the full contract.
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. 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.
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.

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
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 for the full resolution order.

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.

In-Memory Memory (Default)

Even without session_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: 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:

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 readsget_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 locksfcntl.flock on Unix/macOS, msvcrt.locking on 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.

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

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.

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

Where the quarantine lands

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

How to observe it

Subscribe to SESSION_PERSIST_FAILED; the corruption branch reuses the existing payload with distinguishing semantics. 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.

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.
Corruption-quarantine and 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.

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. The DbSessionAdapter 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

Prompt caching is opt-in via 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:
Run your bot:

max_history Resolution

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

Bot Session Configuration

Configure these settings in your bot.yaml under each channel:
When compaction is enabled, max_history becomes a hard upper bound (max_history × 4) instead of the primary trim mechanism. See Bot Session Compaction for the full flow.
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

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

Retention Policies

Long conversations stay safe — older turns are summarised and archived, not silently dropped.
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

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.

Choosing a Policy

Pick the policy that matches what you care about most.

Configuration

Configure retention three ways — from zero-code env vars up to the constructor. Level 1 — env vars (zero code):
Level 2 — constructor:
Level 3 — legacy behaviour (backward-compat trap):
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.

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.

Constructor Parameters

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

Retention Best Practices

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.
When you want a hard cap on disk usage and don’t care about old turns, retention="truncate" hard-slices to the tail.
keep_all never trims — archived_messages and the active window grow unbounded otherwise.
A larger active_window keeps more live context per turn but raises token cost. It defaults to max_messages when unset.
The store logs once when the archive crosses ARCHIVE_WARN_THRESHOLD (10_000) so operators can spot runaway sessions.

Best Practices

Include user or context in the id: f"user-{user_id}-{conversation_type}".
Default max_messages is 100. Overflow is summarised and archived by default (retention="compact") — see Retention Policies.
Call store.delete_session() to remove stale sessions and purge DB rows when using a DB adapter.
Set caching=True on the agent to reduce token cost on repeated conversations.

API Reference

Agent Parameters

DefaultSessionStore Methods

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.

Session Protocol

Build custom session backends

Bot Session Compaction

Summarise older bot turns instead of dropping them

Hook Events

Subscribe to SESSION_PERSIST_FAILED to alert on write failures and corrupt reads