Skip to main content
Durable background jobs persist every state transition so a crash mid-job no longer silently drops the work or the promised deliver-back — orphans become a queryable LOST state and undelivered results are replayed on the next boot. Durability is opt-in: leave store=None (the default) for pure in-memory behaviour with zero overhead; supply a store and jobs survive a restart.

Quick Start

1

Enable persistence (opt-in)

Pass a store= that implements the BackgroundJobStore protocol. Every job transition is now persisted:
2

Reconcile on startup

Call reconcile_on_start() once at boot, after wiring the deliver-back handler:
3

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

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 lives in the wrapper/bot layer alongside the other durable stores (OutboundQueue, DLQ, approvals).
The runner calls upsert() from its worker threads — your store implementation must be thread-safe.

Configuration

Reconciliation counts

reconcile_on_start() returns a counts dict — use it for a single startup log line:

Best Practices

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.
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.
LOST is age-evictable by cleanup_completed(max_age=...). Run a periodic sweep so reconciled orphans don’t accumulate across restarts.
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.
Persistence is opt-in. Omitting store= keeps the runner pure in-memory with zero overhead and unchanged behaviour — only reach for a store when surviving crashes matters.

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.