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.
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.
Quick Start
- 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") - 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
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 tometadata["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” panelpraisonai session resume <id> "<prompt>"— restores state and continues with a new promptpraisonai 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:
- 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 when the id uses only letters, digits, _ and -; other ids get a collision-safe hashed filename — see On-disk filenames.
Behavior Matrix
| 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:
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
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
# 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:
{
"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. |
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:
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 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.
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 fileWrite-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.
| Aspect | Value |
|---|---|
| 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 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 dirfsync, and the final file is chmod’d to0o600. - 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_windowbefore write-back, so recovery respectsmax_messages/active_window/retentionlike 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:
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.
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/…).
| 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.
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:
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.
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
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.
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):
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:
channels:
telegram:
token: ${TELEGRAM_TOKEN}
session:
max_history: 50
reset:
mode: idle
idle_minutes: 30Run your bot:
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:
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. |
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. |
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:
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.
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.
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):
export PRAISONAI_SESSION_RETENTION=compact # default
export PRAISONAI_SESSION_ACTIVE_WINDOW=200 # keep 200 recent turns liveLevel 2 — constructor:
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):
# 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") # -> compactEnvironment 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.
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.
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
| 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 |
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) |