Session Persistence

Automatic session persistence with zero configuration

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

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

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

Diagram

Session Persistence

🤖 Agent

💾 session_id

📁 JSON / DB

⏪ Restore on next run

Quick Start

  1. Start with a session_id
    python
    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")
  2. Resume from the CLI
    bash
    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.


Agent Session Persistence

python
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, praisonai session resume is a first-class restore — not a transcript display.

What is restored:

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

New CLI options:

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

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

For the full CLI reference, see Session Command.

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.


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 when the id uses only letters, digits, _ and -; other ids get a collision-safe hashed filename — see On-disk filenames.

Behavior Matrix

ScenarioBehavior
session_id provided, no DBJSON persistence (automatic)
session_id provided, with DBDB adapter used
No session_id, same Agent instanceIn-memory only
No session_id, new Agent instanceNo history

In-Memory Memory (Default)

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

python
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"

Persistent Sessions

Basic Usage

python
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
# 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
{
  "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:

FieldTypeDescription
modelstringLLM model used in the session
total_tokensintCumulative input+output tokens
costfloatEstimated USD cost
agent_idstringGateway or registry agent id
sourcestringOrigin: chat, gateway, cli, api
agent_namestringHuman-readable agent name
agent_historiesdictPopulated 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.

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

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.

Diagram
SESSION_PERSIST_FAILED~/.praisonai/state/session_spill/Session fileSessionStoreAgentSESSION_PERSIST_FAILED~/.praisonai/state/session_spill/Session fileSessionStoreAgentalt[Write OK][Write fails (disk-full / OSError / corruption)]On next load: re-ingest spills,dedup on (role, content, timestamp),run retention window, then delete files.add_message(…)atomic writeTrueTrueFalseatomic spill (0600, unique filename)fire (role, content, error, spilled, spill_path)False

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.

AspectValue
Spill directory~/.praisonai/state/session_spill/
Filename shape<safe_session_id>.<ms>.<pid>.<random4hex>.json — <safe_session_id> is the collision-safe stem (contains ~<digest> for ids with other characters)
File permissions0o600 (owner read/write only)
Payload{session_id, spilled_at, messages: [SessionMessage.to_dict(), …]}
RecoveryOn next load, re-ingested, deduped on (role, content, timestamp), retention re-enforced
CleanupSpill file deleted only after a successful re-insert
Malformed spillSkipped; does not block recovery of other spills
Happy pathNothing 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
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.

Diagram

Yes

No

💾 Load session

🔍 JSON / UTF-8 decode ok?

✅ Return session

📦 Quarantine to .json.corrupt-<ts>

🪝 Fire SESSION_PERSIST_FAILED

🟢 Return fresh SessionData

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.

Where the quarantine lands

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

AspectValue
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
Renameos.replace(filepath, quarantine_path) under the session FileLock — writes cannot race the quarantine
Best-effortIf the rename itself fails, the quarantine path is None and the hook still fires
Triggersjson.JSONDecodeError or UnicodeDecodeError
Does not triggerOSError 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.

FieldOn corruption
session_idThe affected session id
errorBegins with "corrupt session file: " followed by the decoder error text
spilledRepurposed to carry the quarantine path (or None if the rename failed)
role, contentEmpty strings — there is no in-flight turn to attach
event_name, timestamp, cwdAs 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
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.

Direct Session Store Access

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

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

Encryption at rest

Wrap the store in EncryptedSessionStore to encrypt message content and metadata before they reach disk — keys stay in PRAISONAI_SESSION_KEY.

python
import os
from praisonaiagents.session import get_default_session_store, EncryptedSessionStore

store = EncryptedSessionStore(
    get_default_session_store(),
    key=os.environ["PRAISONAI_SESSION_KEY"],
)

See Encrypted Session Store for key generation and honest limits.

Custom Session Directory

python
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
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):
        # Return the resumed message list (as praisonaiagents.db.DbMessage instances).
        return []

    def on_user_message(self, session_id, content, metadata=None):
        ...  # persist the user turn

    def on_agent_message(self, session_id, content, metadata=None):
        ...  # persist a plain assistant turn

    # Optional — implement these two methods to persist tool-call turns
    # faithfully so a resumed session hands the model the same transcript.
    # Adapters that omit them keep text-only behaviour (unchanged).
    def on_assistant_message(self, session_id, content, tool_calls=None, metadata=None):
        ...  # persist assistant turn WITH tool_calls

    def on_tool_message(self, session_id, content, tool_call_id, metadata=None):
        ...  # persist a role="tool" result linked to tool_call_id

agent = Agent(
    name="Assistant",
    memory=MemoryConfig(session_id="my-session", db=MyDbAdapter()),
)

Context Caching

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

python
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
channels:
  telegram:
    token: ${TELEGRAM_TOKEN}
    session:
      max_history: 50
      reset:
        mode: idle
        idle_minutes: 30

Run your bot:

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

Diagram

set?

not set

set?

not set

No

Yes

channel-level max_history in YAML

Use channel max_history

session.max_history in YAML

Use session.max_history

Default: 100

session.compaction.enabled?

Tail-slice truncate

Compact older turns
max_history × 4 hard cap

Bot Session Configuration

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

SettingYAML keyTypeDefaultDescription
Per-channel history limitmax_historyint100Highest precedence. Caps messages kept per user.
Session-scoped history limitsession.max_historyint100Used when max_history is not set.
Session reset modesession.reset.modestring"none"none, idle, daily, or both.
Idle reset thresholdsession.reset.idle_minutesint60Minutes of inactivity before reset (when mode includes idle).
Daily reset hoursession.reset.at_hourint—Hour (0–23) for daily reset (required when mode includes daily).
Compaction enabledsession.compaction.enabledboolfalseMaster switch for summarising older turns.
Compaction strategysession.compaction.strategystring"summarize"truncate, sliding, summarize, smart, prune, llm_summarize.
Compaction message thresholdsession.compaction.max_messagesint100Approximate threshold (converted to a token budget).
Compaction token budgetsession.compaction.max_tokensint—Optional explicit budget. Overrides max_messages.
Recent tail kept verbatimsession.compaction.keep_recentint10Most-recent messages kept un-summarised.

Session Reset Policies

ModeBehavior
noneSessions never auto-reset (default).
idleResets after idle_minutes of inactivity.
dailyResets once per day at at_hour (0–23).
bothResets on idle or on daily schedule, whichever comes first.

Bounded lock caches

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

Lock typeScopeCleanup
Agent locksOne lock per agent instanceTied to agent lifetime — auto-removed when the agent is garbage-collected
Per-user locksDebounce, session, and run-control pathsBounded 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
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
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

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

Diagram

Compact Retention

No

Yes

📚 New turn

🔍 Over active window?

✅ Append

📝 Summarise oldest

💾 Archive raw

Choosing a Policy

Pick the policy that matches what you care about most.

Diagram

What matters most?

Keep every turn — never lose context

Bounded disk usage — hard cap

Unlimited growth — I compact externally

retention=’compact’ (default)

retention=’truncate’

retention=’keep_all’

Configuration

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

Level 1 — env vars (zero code):

bash
export PRAISONAI_SESSION_RETENTION=compact       # default
export PRAISONAI_SESSION_ACTIVE_WINDOW=200       # keep 200 recent turns live

Level 2 — constructor:

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

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 varTypeValuesEffect
PRAISONAI_SESSION_RETENTIONstringcompact \truncate \keep_allOverrides the global default store’s retention policy. Invalid values are logged and the default (compact) is used.
PRAISONAI_SESSION_ACTIVE_WINDOWintany positive intOverrides the number of recent turns kept live in messages. Non-int values are logged and ignored.

Constructor Parameters

ParameterTypeDefaultDescription
retentionstrNone → compact (or truncate when a non-default max_messages is passed)Overflow policy: compact, truncate, or keep_all. Invalid values raise ValueError.
active_windowintNone → max_messagesNumber 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.

Compacted (archived) turns stay searchable. Both built-in session stores (DefaultSessionStore, SqliteSessionStore) and the exported SqliteTranscriptStore scan a shared archived + active projection, so a token that survives only in archived_messages after compaction is still recallable via session_search and the praisonai session search CLI. Every returned context message carries an archived: bool flag so the compaction boundary stays visible. See Cross-Session Recall → Recall Across Compacted History.

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.

Diagram
DefaultSessionStoreAgentUserDefaultSessionStoreAgentUserretention=”compact”, active_window=100log: “compacted 1 early turns into a summary”Turn 100save(session)_enforce_window → within window, appendTurn 101save(session)_enforce_window → summarise oldest, archive raw(later) resume sessionget_chat_history()summary + recent turnscontinues with context preserved

Retention Best Practices

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.

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.

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.

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.

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.

Best Practices

Use meaningful session IDs

Include user or context in the id: f"user-{user_id}-{conversation_type}".

Respect the default message limit

Default max_messages is 100. Overflow is summarised and archived by default (retention="compact") — see Retention Policies.

Clean up unused sessions

Call store.delete_session() to remove stale sessions and purge DB rows when using a DB adapter.

Enable prompt caching for Anthropic

Set caching=True on the agent to reduce token cost on repeated conversations.

API Reference

Agent Parameters

ParameterTypeDescription
session_idstrSession identifier for persistence
dbDbAdapterOptional database adapter (overrides JSON)
prompt_cachingboolEnable Anthropic prompt caching

DefaultSessionStore Methods

MethodDescription
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 active messages (reload-under-lock). Retains the JSON transcript metadata, archived_messages, gateway_session_id, agent_id, and (on SqliteSessionStore) all three index tables. Use delete_session to deindex and remove the session.
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