WOLBΛRG

Concurrency

Multi-process SQLite write safety in Wolbarg 0.4 — BEGIN IMMEDIATE, busy_timeout, exponential backoff, WOLBARG_STORAGE_LOCKED, and published multi-writer benchmarks.

What is it?

Wolbarg 0.4 hardens multi-process writers against a single SQLite memory.db — the topology most multi-agent setups use (one Node process or worker per agent, one shared file).

Before 0.4, SQLite mutating paths used deferred BEGIN. Under concurrent writers that commonly produced SQLITE_BUSY at commit time after work had already started. 0.4 acquires the write lock up front and retries with backoff when the lock is contended.

Guarantees

GuaranteeDetail
WAL modeEnabled by default so readers do not block writers
BEGIN IMMEDIATEMutating transactions take the write lock before doing work
Busy timeoutSQLite busy_timeout pragma honors lockTimeoutMs
App-level retryOn SQLITE_BUSY, exponential backoff + jitter up to maxRetries
Stable errorExhausted retries throw WOLBARG_STORAGE_LOCKED with an actionable suggestion
In-process mutexSame-process concurrent writers are additionally serialized

PostgreSQL is unchanged for this feature — it already uses row-level locking and connection pooling. Concurrency config is ignored on the Postgres provider.

Why it matters for agents

Multi-agent frameworks often open the same SQLite file from several processes:

  • Parallel tool workers writing preferences / facts
  • Dev servers + background jobs sharing local memory
  • CLI agents launched side-by-side against one project DB

Without write-lock discipline, you get intermittent failures, orphaned retries, or corrupted mental models when agents silently drop writes. Wolbarg 0.4 makes contention visible, retryable, and bounded.

Configuration

import { wolbarg, openaiEmbedding } from "wolbarg";

const ctx = wolbarg({
  organization: "my-org",
  database: { provider: "sqlite", url: "./memory.db" },
  embedding: openaiEmbedding({
    apiKey: process.env.OPENAI_API_KEY!,
    model: "text-embedding-3-small",
  }),
  concurrency: {
    maxRetries: 5,       // default — app-level retry attempts after SQLITE_BUSY
    baseBackoffMs: 50,   // default — first backoff floor
    maxBackoffMs: 2000,  // default — backoff ceiling
    lockTimeoutMs: 5000, // SQLite busy_timeout pragma (ms)
  },
});

Defaults are conservative for a handful of concurrent agent processes. Raise maxRetries / lockTimeoutMs for bursty writers; switch to Postgres when you need high fan-out multi-tenant write throughput.

Defaults

FieldDefaultMeaning
maxRetries5How many times Wolbarg retries a busy transaction
baseBackoffMs50Starting delay before retry (grows exponentially)
maxBackoffMs2000Cap on backoff delay
lockTimeoutMs5000Passed to SQLite busy handler / busy_timeout

How it works (internals)

  1. A mutating path (remember, update, forget, ingest writes, compress archive, …) opens a transaction with BEGIN IMMEDIATE.
  2. If another process holds the write lock, SQLite returns busy within lockTimeoutMs.
  3. Wolbarg sleeps with exponential backoff + jitter and retries up to maxRetries.
  4. If all attempts fail, it throws WOLBARG_STORAGE_LOCKED — not a generic SQLite string.
  5. Same-process callers also queue through an in-process mutex so Node async concurrency does not pile on top of OS-level lock storms.

Tradeoffs

UpsideCost
Far fewer surprise SQLITE_BUSY failuresp95/p99 latency rises under contention
Bounded, actionable errorsVery high writer fan-out still serializes on one file
Works with existing local-first topologiesPostgres remains better for SaaS-scale concurrent writes

Tune for your workload, or move shared multi-tenant agents to the PostgreSQL backend.

Errors

CodeWhenWhat to do
WOLBARG_STORAGE_LOCKEDWrite-lock retries exhaustedIncrease maxRetries / lockTimeoutMs, reduce writer fan-out, or switch to Postgres

Suggestion text on the error points at the same remediation.

Benchmarks

Same-process stress (v4 suite)

Published dual-backend v0.4 stress numbers (SQLite · Postgres):

WritersSQLite throughputSQLite p95Postgres throughputPostgres p95
8 × 20 ops6,084 ops/s3.05 ms2,555 ops/s8.51 ms
16 × 20 ops8,660 ops/s2.46 ms3,335 ops/s9.48 ms
32 × 20 ops6,798 ops/s22.94 ms3,802 ops/s14.77 ms

Mock embeddings; Node v24 · win32/arm64 · 8 CPUs · generated 2026-07-18. Absolute ops/sec vary by machine.

Multi-process levels

True multi-process writers (separate OS processes, shared SQLite file):

npx tsx benchmark/multiprocess-levels.ts

Artifact: /benchmarks/multiprocess-concurrency.json — levels 2 / 5 / 10 / 20 writers with throughput, p50/p95/p99, error rate, and integrity checks. At 20 writers this run held 0% error rate with integrity OK (p95 rises as expected under serialization).

Interactive charts: /benchmarks. Methodology: Benchmarks.

When to use Postgres instead

Choose PostgreSQL when:

  • Many agents share one cluster across hosts
  • You need cross-process real-time events (LISTEN/NOTIFY)
  • Write fan-out regularly exceeds what a single SQLite file can serialize comfortably