LOST state and undelivered results are replayed on the next boot.
Durability is on by default: the shared manager returned by get_job_manager() — the same one the chat /tasks command and background subagent deliver-backs use — automatically persists to <runs_dir>/background_jobs.db and reconciles on boot. Opt out with PRAISONAI_BACKGROUND_JOB_STORE=0 for the prior pure in-memory behaviour.
Three ways to reach durability — pick the row that matches your setup.
Quick Start
1
You already have durability
The shared manager returned by
get_job_manager() is already backed by SqliteBackgroundJobStore and has already reconciled orphaned jobs on first construction — zero code, zero setup:2
Enable persistence on a custom manager
Building your own
BackgroundJobManager? Pass store=SqliteBackgroundJobStore() (or any store implementing the BackgroundJobStore protocol). Every job transition is now persisted:3
Reconcile on startup
get_job_manager() already ran this once. On a custom manager, call reconcile_on_start() once at boot, after wiring the deliver-back handler:4
Query a job after a restart
Status lookups work after a restart — even before
reconcile_on_start() runs, get_status falls through to the store:5
Handle LOST jobs
Inspect orphaned jobs and decide whether to retry or surface them to the user — abandoned work is never auto-re-run:
How It Works
reconcile_on_start() reads store.list_unreconciled() and follows two paths: orphaned jobs become LOST; completed-but-undelivered jobs are replayed.
Every persisted job is re-hydrated into the in-memory map so get_status(job_id) keeps working after a restart. redeliver=None is safe — undelivered jobs are re-hydrated but not delivered.
State machine
LOST is a new terminal state reached only via restart reconciliation.
The BackgroundJobStore Protocol
Implement this@runtime_checkable protocol to plug in your own durable store. A concrete SQLite implementation — SqliteBackgroundJobStore — now ships in core (praisonaiagents.background) and backs get_job_manager() by default; you only implement the protocol yourself for a non-SQLite backend.
Built-in SqliteBackgroundJobStore
SqliteBackgroundJobStore is the concrete, stdlib-only store that backs get_job_manager() by default. Import it, or omit db_path to use the default location:
Schema
A singlebackground_jobs table with an index on status:
Index:
idx_background_jobs_status on status.
Thread-safety & durability posture
A single re-entrant lock guards one shared connection openedcheck_same_thread=False — safe to share across the runner’s worker threads. Durability matches its siblings SQLiteRunLedger and SqliteEventLog: WAL journal mode, busy_timeout=5000, synchronous=NORMAL.
Configuration
Environment variable
Reconciliation counts
reconcile_on_start() returns a counts dict — use it for a single startup log line:
Best Practices
Call reconcile_on_start() exactly once, at startup
Call reconcile_on_start() exactly once, at startup
Run it once at boot, after all deliver-back handlers are wired — mirroring how
OutboundQueue.drain_pending() is wired. Calling it later risks a partially-wired redeliver.Make redeliver idempotent
Make redeliver idempotent
redeliver receives the persisted JobInfo. A transient failure inside it means “retry on the next boot” — the job is left undelivered, never silently lost. Design the handler so a double-fire is harmless.Sweep LOST records with cleanup_completed()
Sweep LOST records with cleanup_completed()
LOST is age-evictable by cleanup_completed(max_age=...). Run a periodic sweep so reconciled orphans don’t accumulate across restarts.Keep the store thread-safe
Keep the store thread-safe
upsert() is called from BackgroundJobManager’s worker threads. Guard shared state (or use a per-thread connection) so concurrent transitions don’t corrupt the store.Opt out with the env var if you truly want in-memory
Opt out with the env var if you truly want in-memory
For the shared
get_job_manager() the escape hatch is the environment variable, not the constructor kwarg — set PRAISONAI_BACKGROUND_JOB_STORE=0 to get pure in-memory behaviour (no store, no reconcile). When you construct BackgroundJobManager yourself, omitting store= keeps that manager in-memory with zero overhead.Related
Background Tasks
The synchronous job manager and background runner patterns.
Background Subagents
Spawn subagents that deliver results back to chat when done.
Durable Delivery
Persist outbound bot messages with retry and crash-safe drain.
Hook Events
Subscribe to JOB_COMPLETED and other lifecycle events.

