Gateway Session Portability
Back up, migrate, and restore gateway conversation sessions
Export gateway conversations to a portable JSON file so you can back them up, move them between hosts, or restore them after disaster.
Quick Start
- Back up and restore from the CLI
Export every session to one file, then restore it on any host.
bash praisonai gateway sessions export --out backup.json praisonai gateway sessions import --in backup.json # inert until re-bound - Do it from Python
The gateway helpers wrap the same store the gateway runs on.
python from praisonai_bot.gateway.preflight import export_gateway_sessions, import_gateway_sessions payload = export_gateway_sessions() report = import_gateway_sessions(payload) print(report["imported"], report["skipped_count"])
How It Works
Export reads live sessions into a versioned payload; import writes them back and resets live routing fields so restored state stays inert until the gateway re-binds it.
| Behaviour | Detail |
|---|---|
| Payload shape | Versioned envelope {"version": 1, "sessions": [...]} built from SessionData.to_dict() |
| Lineage-aware | Compacted / rotated continuations round-trip as one logical session (lineage_id / root_session_id / thread_id) |
| Hardened import | max_sessions cap, duplicate/cycle guard, malformed records skipped and reported |
| Version guard | Rejects payloads newer than PORTABLE_VERSION = 1; older/unversioned accepted best-effort |
| Live-field reset | Clears gateway_session_id and agent_id so restored state is inert until re-bound |
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"Choosing the right options
Pick a scope, decide on lineage, and choose whether to overwrite.
CLI Reference
export
praisonai gateway sessions export [OPTIONS]
Examples:
praisonai gateway sessions export --out backup.json
praisonai gateway sessions export -s bot_slack_U123 -o one.json
praisonai gateway sessions export -s bot_slack_U123 --no-lineage| Flag | Type | Default | Description |
|---|---|---|---|
--session-id, -s | str? | None | Export just this session (lineage-aware); default = all |
--out, -o | str? | stdout | Atomic write via temp file + os.replace |
--no-lineage | bool | False | Skip compacted/rotated ancestors for a single-session export |
import
praisonai gateway sessions import [OPTIONS]
Examples:
praisonai gateway sessions import --in backup.json
praisonai gateway sessions import -i backup.json --overwrite
praisonai gateway sessions import -i backup.json --keep-live-fields --max-sessions 500 --json| Flag | Type | Default | Description |
|---|---|---|---|
--in, -i | str | required | Read payload from this file |
--overwrite | bool | False | Overwrite sessions that already exist |
--keep-live-fields | bool | False | Do not reset live routing/activity fields |
--max-sessions | int | 10_000 | Cap ingest |
--json | bool | False | Output the ImportReport as JSON |
Python API Reference
The same surface exists on the store and as thin gateway helpers.
from praisonaiagents.session import DefaultSessionStore, ImportReport
store = DefaultSessionStore()
# Export
payload = store.export_session("bot_slack_U123", include_lineage=True) # {"version": 1, "sessions": [...]}
payload = store.export_all() # {"version": 1, "sessions": [...]}
# Import
report: ImportReport = store.import_sessions(
payload,
max_sessions=10_000,
reset_live_fields=True,
overwrite=False,
)Gateway helpers target the store the gateway actually runs on:
from praisonai_bot.gateway.preflight import export_gateway_sessions, import_gateway_sessions
payload = export_gateway_sessions(session_id=None, include_lineage=True)
report = import_gateway_sessions(payload, max_sessions=10_000, reset_live_fields=True, overwrite=False)DefaultSessionStore, SqliteSessionStore, and SqliteTranscriptStore (the default gateway backend) all implement PortableSessionStoreProtocol. The payload format is identical across all three — a backup from one restores cleanly into another.
| Method | Signature | Returns |
|---|---|---|
export_session | (session_id, *, include_lineage=True) | dict — versioned payload |
export_all | () | dict — versioned payload |
import_sessions | (payload, *, max_sessions=10_000, reset_live_fields=True, overwrite=False) | ImportReport |
ImportReport is JSON-serialisable via as_dict():
{
"imported": 3,
"skipped": [{"session_id": "s1", "reason": "already exists (use overwrite)"}],
"skipped_count": 1,
"version": 1
}Payload format
The envelope is versioned and symmetric across the built-in stores.
{
"version": 1,
"sessions": [
{ "session_id": "bot_slack_U123", "messages": [], "metadata": {} }
]
}Each session dict is the SessionData.to_dict() shape, so SessionData.from_dict() reconstructs it verbatim on import.
Concurrency & no-clobber guarantee
Concurrent importers racing on the same session_id are safe: one wins, the other is reported — never silently overwritten.
from praisonaiagents.session import SqliteSessionStore
store = SqliteSessionStore(db_path="~/.praisonai/sessions.db")
# Safe: concurrent importers cannot clobber each other.
# One wins; the other sees it in report.skipped.
report = store.import_sessions(payload, overwrite=False)
print(report.imported, report.skipped)Both DefaultSessionStore and SqliteSessionStore enforce this guarantee identically.
overwrite=False is checked twice. The import loop first scans with session_exists, then _save_imported_session rechecks os.path.exists inside the per-file write lock — so a peer that creates the same session between the two checks still cannot be clobbered.
sequenceDiagram
participant A as Importer A
participant B as Peer writer B
participant S as Store (lock + disk)
A->>S: session_exists("shared")? false
B->>S: write "shared" (creates file)
A->>S: _save_imported_session("shared", overwrite=False)
Note over S: acquire FileLock
S-->>A: FileExistsError (recheck inside lock)
Note over S: release lock
A-->>A: skip with reason "already exists (use overwrite)"
classDef importer fill:#8B0000,stroke:#7C90A0,color:#fff
classDef lock fill:#189AB4,stroke:#7C90A0,color:#fff
classDef outcome fill:#10B981,stroke:#7C90A0,color:#fff
class A,B importer
class S lockThe loser lands in ImportReport.skipped as {"session_id": "shared", "reason": "already exists (use overwrite)"}. As of PraisonAI PR #5631, SqliteSessionStore honours overwrite end-to-end, so this protection holds identically whether the destination is JSON-on-disk or the SQLite-indexed store.
Common Patterns
Daily backup — schedule a cron job that writes one dated file per day.
praisonai gateway sessions export --out /var/backups/gw-$(date +%F).jsonStaging → production migration — export from staging, then overwrite only the ids being cut over.
praisonai gateway sessions export -s bot_slack_U123 -o cutover.json # on staging
praisonai gateway sessions import -i cutover.json --overwrite # on productionMove one user’s conversation between hosts — export a single lineage-aware session, plain-import on the new host.
praisonai gateway sessions export -s bot_slack_U123 -o one.json # old host
praisonai gateway sessions import -i one.json # new hostRestore after disaster — import a backup; live fields reset by default, so sessions stay inert until the gateway re-binds them.
praisonai gateway sessions import --in backup.jsonBest Practices
Keep reset_live_fields=True unless you know why not
Resetting clears gateway_session_id and agent_id (top-level and inside metadata) so a restored session cannot masquerade as an active connection. Only opt out with --keep-live-fields when re-importing onto the exact same live gateway.
Cap ingest for untrusted payloads
--max-sessions (default 10_000) bounds how many sessions land. The remainder is skipped and reported — never silently truncated.
Always inspect ImportReport.skipped
A skipped record — already-exists, malformed, or over the cap — is listed with a reason, never dropped in silence. Read skipped before assuming a clean restore.
Prefer –out over stdout redirection
--out writes atomically via a temp file + os.replace, so a crashed export never leaves a half-written backup.
Mind payload version compatibility
PORTABLE_VERSION = 1. Older/unversioned payloads are accepted best-effort; a payload with a newer version is rejected wholesale rather than partially applied.
SqliteTranscriptStore export/import works on DB rows
As of PraisonAI PR #5686 (issue #5517), SqliteTranscriptStore.export_all scans rows in sessions.db and ignores stray *.json sidecars; import_sessions writes DB rows and reports a read-only database (PRAGMA query_only=ON) as a failed write in ImportReport.skipped instead of silently writing a sidecar. Lineage-aware exports (lineage_id / root_session_id / thread_id) follow DB continuations end-to-end.
Troubleshooting
import error: … unexpected keyword argument 'overwrite'
Fixed in PraisonAI PR #5631. Upgrade praisonaiagents and re-run the import; SqliteSessionStore now accepts and honours the overwrite flag end-to-end.
export skipped DB sessions / import wrote an invisible sidecar
Fixed in PraisonAI PR #5518 and PR #5686. Before the fix, SqliteTranscriptStore inherited JSON-sidecar implementations of export_all / _collect_lineage / _save_imported_session — exports omitted every DB session and imports silently wrote to invisible .json sidecars. Upgrade praisonaiagents and re-run the export/import; the DB-row path is now used end-to-end.