LOST so you can wake the user and re-route.
Complements — does not replace — the Run-State Journal. The ledger tracks run status (queued/running/done/failed/lost); the journal tracks the per-event cursor (model decision, tool call, tool result, iteration index) so a crashed run can resume without re-executing tools or re-billing LLM calls. Use the ledger to answer “is this run alive?”; use the journal to answer “where in the loop did it die?”
~/.praisonai/runs/ledger.db.
Quick Start
1
Get the default ledger
2
Track a run
3
Reconcile on startup
4
Guarantee exactly-once wake-back
recover_orphans() returns every undelivered LOST run — both those reconciled in this call and any left undelivered by a prior boot. It preserves each run’s channel and thread_id so the gateway can wake the same user back.Retry on next boot is automatic. If
deliver_terminal returns False (or raises), the run stays delivered = False. The next boot’s recover_orphans() will surface it again so a working transport can wake the user without any manual re-arming.How It Works
A run is recorded as it starts, updated as it progresses, and finalised with a terminal status. If a process dies while a run is still active, the next boot reconciles it toLOST.
Behaviour on gateway restart
The gateway wires this recovery automatically — you don’t callrecover_orphans() yourself. On boot, after resuming interrupted turns, it reconciles the ledger and notifies each lost run’s origin.
_recover_orphaned_runs()runs on boot, right after_resume_interrupted_turns().recover_orphans()terminalises every still-active run toLOST, preserving itschannelandthread_id.- Each
LOSTrun’s origin receives a durable restart notice. The gateway calls the transport, and only on a successful landing marks the rundeliveredin the ledger — so a failed/raising transport (rather than being silently dropped) is leftdelivered = Falseand automatically retried on the next boot. When no origin route exists (emptychannel), the run is markeddeliveredimmediately to stop the ledger re-surfacing an undeliverable run on every subsequent boot. - It’s a no-op when core lacks the ledger or
ledger.dbdoesn’t exist yet — gateways that never used the ledger are unaffected, and no empty DB is ever created.
Exactly-once wake-back
ALOST run stays owed a wake-back until its notice actually lands — surviving a transport outage across boots without ever double-notifying.
The user-facing message the origin receives:
This is the built-in gateway behaviour. The manual patterns below still work if you build your own runtime on the ledger, but a standard
praisonai gateway start already does this for you. See also Gateway Restart Continuation › Run ledger recovery.Run statuses
RunStatus partitions every run into active (recoverable) or terminal (done).
Check the partition with the
is_active / is_terminal helpers:
RunStatus is a str enum, so RunStatus.RUNNING == "running".
Configuration Options
RunRecord
A durable record of a single run.channel and thread_id capture the origin route so the gateway can wake the user back.
to_dict() / from_dict() roundtrip a record to a JSON/SQLite-friendly dict and back.
RunLedgerProtocol
The pluggable store contract — swap in a heavier backend by implementing these methods.mark_delivered(run_id) records that this run’s terminal outcome reached its origin. Once set, recover_orphans() will never return this run again — the durable half of the exactly-once guarantee.
TerminalOutcomeDelivererProtocol
The transport contract the wrapper implements. Core owns the exactly-once guarantee; the wrapper supplies only the concrete transport.notify_recovered
notify_recovered(ledger, deliverer) is the async binder that closes the loop. For each record from recover_orphans() it invokes deliverer.deliver_terminal(record) and, only on success, calls ledger.mark_delivered(record.run_id). A raising transport is treated as a failed (retryable) delivery, never crashing recovery for the other runs. Returns the number of runs successfully delivered this call.
SQLiteRunLedger
The zero-dependency default, backed by stdlibsqlite3.
- No new dependencies — stdlib
sqlite3only. - Thread-safe — a re-entrant lock guards a shared WAL connection.
recover_orphans()preserves the origin route and returns every undeliveredLOSTrun; oncemark_deliveredflips a run, a later call filters it out.close()releases the connection; the file persists across restarts.
Automatic in-place migration. Older
ledger.db files from before this release lack the delivered column. Opening the ledger runs an additive ALTER TABLE runs ADD COLUMN delivered INTEGER NOT NULL DEFAULT 0 — existing rows default to delivered = False (safe: they get retried once), and every subsequent open is a no-op. Nothing to run manually.Common Patterns
Mark a run terminal on success
Manual integration (custom hosts)
When you runpraisonai gateway start, boot recovery is automatic (see Automatic on gateway boot) — you do not need this pattern. It remains the way to wire recovery into a custom, non-gateway process.
List recent runs regardless of status
Best Practices
The gateway calls recover_orphans() for you at boot
The gateway calls recover_orphans() for you at boot
A standard
praisonai gateway start runs reconciliation automatically on boot, before accepting new work, and notifies each lost run’s origin durably. Only call recover_orphans() yourself when you build a custom runtime on the ledger directly.Always set channel and thread_id
Always set channel and thread_id
These fields are the only way the gateway can wake the right user back. Set them when the run starts so a later
LOST reconciliation can reach the origin thread.Update status at each transition
Update status at each transition
Call
upsert() as the run moves queued → running → waiting → succeeded/failed. The more current the status, the fewer false LOST reconciliations after a restart.Swap the store for heavier backends
Swap the store for heavier backends
SQLiteRunLedger is the default, but any object implementing RunLedgerProtocol (Postgres, Redis, a hosted queue) drops in unchanged — the gateway only depends on the protocol.Return the truth from deliver_terminal
Return the truth from deliver_terminal
Have
deliver_terminal return True only when the message actually landed on the origin (or was durably enqueued to a store that will land it). Returning True optimistically breaks the exactly-once guarantee — a transient network blip becomes a permanently lost wake-back. When the send fails, return False (or let the exception propagate) so the ledger retries it next boot.What The User Sees
1
User asks for a long task
A user messages a Telegram bot: “Research the top 10 databases and write a comparison.” The agent starts, and the run is recorded as
RUNNING with channel="telegram" and the user’s thread_id.2
PraisonAI restarts mid-run
The process is killed or crashes before the run finishes. In-memory state is gone, but the ledger row on disk survives.
3
The gateway wakes the user back
On boot, the gateway automatically calls
recover_orphans(), marks the run LOST, and posts a durable notice back to the same Telegram thread. If Telegram is unreachable at that instant, the notice stays owed — and the next boot delivers it. Once the user has been told, they are never re-notified for the same run, however many times PraisonAI restarts.Related
Background Tasks
Run agent work in the background and collect results later.
Background Subagents
Spawn subagents that return a job ID immediately — the general-purpose ledger backs their durable state.
Restart Continuation
The boot recovery that runs ledger reconciliation alongside interrupted-turn resumption.

