{"id":"03284a51-b69c-42b4-91b7-92ac4c922514","slug":"simplebroker","kind":"skill","name":"simplebroker","summary":"A lightweight, no-configuration message broker/queue for cli tools","owner":"VanL","repoUrl":"https://github.com/VanL/simplebroker","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"skill","createdAt":"2025-07-06T23:44:28.000Z","addedAt":"2026-10-21T15:32:56.621Z","logoUrl":"https://avatars.githubusercontent.com/u/214698?u=9ae883ede9287d3f2db6469be7b2f7a0a69ca560&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["writing"],"tags":[],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T20:40:16.691Z","sample":false,"url":"https://conduits.dev/tools/simplebroker","language":"Python","transports":["stdio"],"install":{"pypi":"simplebroker"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# SimpleBroker\n\n  [![CI](https://github.com/VanL/simplebroker/actions/workflows/test.yml/badge.svg)](https://github.com/VanL/simplebroker/actions/workflows/test.yml)\n  [![codecov](https://codecov.io/gh/VanL/simplebroker/branch/main/graph/badge.svg)](https://codecov.io/gh/VanL/simplebroker)\n  [![PyPI version](https://badge.fury.io/py/simplebroker.svg)](https://badge.fury.io/py/simplebroker)\n  [![Python versions](https://img.shields.io/pypi/pyversions/simplebroker.svg)](https://pypi.org/project/simplebroker/)\n\n*A lightweight message queue backed by SQLite. No server, no daemon, no\ndependency resolver surprises.*\n\n```bash\n$ pipx install simplebroker\n$ broker write tasks \"ship it\"\n$ broker read tasks\nship it\n```\n\nSimpleBroker exists for the space between shell pipes and a real broker fleet:\nlocal automation, agents, cron jobs, test harnesses, small services, and\nproject-local coordination that need durable queue semantics without operating\nRedis, RabbitMQ, or a cloud service. The default install has no runtime\ndependencies and stores its state in one SQLite database.\n\n## Recommended For\n\n- **Python projects that need a queue without infrastructure.** Most queue\n  stacks assume Redis, RabbitMQ, Celery, or a managed service. SimpleBroker's\n  default install does not. That matters for tools shipped to users who should\n  not have to set up a queue server. Zero configuration: no servers, no\n  daemons; each directory gets its own isolated `.broker.db` with ACID\n  durability and safe concurrent access. On an M4 MacBook Pro,\n  [`bin/benchmark.py`](bin/benchmark.py) measured 962.8 mixed ops/second\n  through the default API and 7,879.6 through the persistent optimized API.\n- **Shell scripts, cron jobs, and CI/CD pipelines.** `broker write tasks\n  \"build #123\"` composes with pipes, exit codes, and `--json` like a Unix\n  tool — decouple script stages, coordinate build steps, buffer logs, or\n  pass work between processes on one machine.\n- **Coding agents that need a queue primitive.** The CLI gives agents a durable\n  coordination point without an MCP server, daemon, or project-specific setup.\n- **Library and tool authors embedding queue semantics.** Use a small client or\n  context object over SimpleBroker, resolve your settings into one `Config`\n  (under your own prefix if you like), and hand out queues bound to one\n  resolved broker target. Weft is the reference implementation of this pattern.\n- **Event-driven workflows** via the built-in real-time watcher.\n\n**Not for:** Broker fleets, pub/sub, distributed task frameworks, or applications\nneeding very high scale or throughput (like high-frequency trading).\n\n## Table of Contents\n\n- [SimpleBroker](#simplebroker)\n  - [Recommended For](#recommended-for)\n  - [The SimpleBroker Model](#the-simplebroker-model)\n  - [The SimpleBroker API](#the-simplebroker-api)\n  - [Project Specifications and Agent Instructions](#project-specifications-and-agent-instructions)\n  - [Installation](#installation)\n  - [Quick Start](#quick-start)\n  - [Command Reference](#command-reference)\n  - [Critical Safety Notes](#critical-safety-notes)\n  - [Core Concepts](#core-concepts)\n  - [Common Patterns](#common-patterns)\n  - [Real-time Queue Watching](#real-time-queue-watching)\n  - [Embedding SimpleBroker in Your Project](#embedding-simplebroker-in-your-project)\n  - [Performance & Tuning](#performance--tuning)\n  - [Project Scoping](#project-scoping)\n  - [Going Further](#going-further)\n  - [License](#license)\n\n## The SimpleBroker Model\n\nSimpleBroker's core concept is the durable named queue - basically Unix pipes\nmade durable, inspectable, and resumable. Cooperating processes exchange \nmessages through named queues on one resolved broker target (the \"backend\"). \n\n- SimpleBroker owns the semantics of the queue operations: write, claim,\npeek, move, delete, watch. \n- The backend owns storage and topology. SQLite is the default. Optional Postgres\nor Redis/Valkey services can also be used and provide their own replication,\navailability, and recovery features, separate from SimpleBroker.\n- The application or script using SimpleBroker owns what messages mean: task\nexecution, business retries, worker topology, and completion.\n\nSimpleBroker's one concession to application state is the ability to route \n`sidecar` tables to the same backend (on the SQLite and pg backends). This\nallows embedders to use the same configuration information and connection - but\nSimpleBroker does not constrain what is in sidecar tables. SQL schema migration\nmay rebuild only reserved broker objects and leaves caller-owned sidecar schema\nand state unchanged on success or failure. Additions to `messages`, `meta`, the\naliases table, or broker-owned indexes are reserved-object changes, not\nsidecars, and are unsupported. See\n[`SB-API-7`](docs/specs/16-python-library-api.md#backend-packaging-and-sidecar-boundary-sb-api-7).\n\n## The SimpleBroker API\n\nThe SimpleBroker API is designed to mirror the CLI: `broker write tasks \"hi\"` \nand `Queue(\"tasks\").write(\"hi\")` mean the same queue operation over the same \nresolved target.\n\nThe CLI reads its configuration and tuning settings from `BROKER_*` environment\nvariables once at startup. In Python, the same settings live in one read-only\n`Config` built with `resolve_config()` and passed to functions and classes;\n`resolve_config(env=os.environ)` reads the same variables. Embedders can use\ntheir own prefix, such as `APP_CACHE_MB`, to avoid name clashes.\n\nThe public API consists of the names exported from `simplebroker`,\n`simplebroker.ext`, and the `simplebroker.commands.__all__` command layer\ndescribed under [Embedding SimpleBroker in Your Project](#embedding-simplebroker-in-your-project).\nDirect command functions return integer codes for ordinary outcomes and raise\nPython exceptions for invalid input or operational failures; only the CLI\ntranslates those failures to diagnostics and exit `1`.\nUnderscore-prefixed modules are implementation details and may change.\nSpecifications: `docs/specs/16-python-library-api.md` (`[SB-API-1]`–`[SB-API-15]`).\n\n```python\nfrom simplebroker import Queue\n\nwith Queue(\"jobs\") as queue:\n    message_id = queue.write(\"current state\", keep_newest=10)\n    oldest = queue.peek_one()\n    newest = queue.peek_one(order=\"newest\")\n    recent = queue.peek_many(limit=10, order=\"newest\")\n```\n\nThe keep-write call still returns only the new committed message ID.\n\nBounded read, peek, and move operations select the lowest public message ID\nfirst by default. `order=\"newest\"` selects highest ID first. The option is not\navailable on generators or `all_messages=True`; see\n[`SB-SELECT-5`](docs/specs/14-timestamp-selection.md#cross-backend-retrieval-order-sb-select-5).\n\n## Project Specifications and Agent Instructions\n\nExact product behavior is owned by the canonical specs in `docs/specs/` \n(`10-cli.md` … `17-ops.md`, codes `[SB-CLI-*]` … `[SB-OPS-*]`), registered in\n`docs/specs/product-section-registry.md`. \n\nAgents should use `docs/agent-kernel.md` for use orientation. The `AGENTS.md`\nfile is the entry point for agents doing work on this codebase, including the\n[program-theory.md](https://github.com/VanL/simplebroker/blob/main/docs/program-theory.md)\nand the broader agent context and runbooks.\n\n## Installation\n\n```bash\n# Use pipx for global installation (recommended)\npipx install simplebroker\n\n# Or install with optional Postgres support\npipx install \"simplebroker[pg]\"\n\n# Or install with uv to use as a library\nuv add simplebroker\nuv add \"simplebroker[pg]\"\n\n# Or with pip\npip install simplebroker\npip install \"simplebroker[pg]\"\n```\n\nThe CLI is available as both `broker` and `simplebroker`.\n\n**Requirements:**\n- Python 3.11+\n- SQLite 3.35+ (released March 2021) - required for `RETURNING` support\n\nStandard, GIL-enabled CPython 3.15 is supported on Linux, macOS, and Windows.\nThis qualification does not cover free-threaded Python 3.15t or JIT-specific\nbehavior.\n\n\n## Quick Start\n\n```bash\n# Write a message\n$ broker write myqueue \"Hello, World!\"\n\n# Superseding snapshot/state feeds can atomically claim older pending values\n# with --keep-newest. Read its destructive-state warning under Write options.\n\n# Read the message (removes it)\n$ broker read myqueue\nHello, World!\n\n# Write from stdin\n$ echo \"another message\" | broker write myqueue\n$ echo \"another message\" | broker write myqueue -\n\n# Read all messages at once\n$ broker read myqueue --all\n\n# Peek without removing\n$ broker peek myqueue\n\n# Select the highest eligible public message ID for one bounded operation\n$ broker peek jobs --newest\n$ broker read jobs --newest\n$ broker move jobs archive --newest\n\n# Move messages between queues\n$ broker move myqueue processed\n$ broker move errors retry --all\n\n# Rename existing queue state\n$ broker rename processed archive.ready\n$ broker rename @legacy archive.ready --json\n\n# list all queue names\n$ broker list\nmyqueue\nprocessed\n$ broker list --stats\nmyqueue: 3\nprocessed: 1\n$ broker stats myqueue\nmyqueue: 3\n$ broker exists myqueue\n$ broker list --prefix jobs. --stats\n\n# Broadcast to all queues\n$ broker broadcast \"System maintenance at 5pm\"\n# Target only matching queues using fnmatch-style globs\n$ broker broadcast --pattern 'jobs-*' \"Pipeline paused\"\n# Target an exact set of existing literal queues; --queue is repeatable\n$ broker broadcast \\\n    --queue notify.alice \\\n    --queue notify.carol \\\n    \"Thread updated\"\n\n# After stopping all users, destructively remove the target state\n$ broker --cleanup\n```\n\n## Command Reference\n\nResidual queue/broker operations (existence, metadata, delete, rename, aliases,\nvacuum, destructive target cleanup): `docs/specs/17-ops.md`\n(`[SB-OPS-1]`–`[SB-OPS-7]`). Delivery, identity, selection, broadcast,\ndump/load, CLI packaging, and library surfaces have their own specs (see\n`docs/specs/product-section-registry.md`).\n\n### Global Options\n\nGlobal options must appear before the command, for example `broker -f queue.db read jobs`.\n\n- `-d, --dir PATH` - Use PATH instead of current directory\n- `-f, --file NAME` - Database filename or absolute path (default: `.broker.db`)\n  - SQLite names use ASCII letters, digits, dot, dash, and underscore; existing\n    traversal and platform checks also apply. See [filename rules](docs/specs/10-cli.md#stdout-and-stderr-sb-cli-2).\n  - If an absolute path is provided, the directory is extracted automatically\n  - Cannot be used with `-d` if the directories don't match\n- `-q, --quiet` - Suppress non-error human commentary, including the plain\n  message-newline and alias-shadow warnings. Payload, errors, and unrelated\n  Python warnings remain visible.\n- `--cleanup` - Destructively delete the configured backend target state and\n  exit. SQLite cleanup attempts the database and its known SQLite and\n  SimpleBroker companion files. It is non-atomic; stop all activity and make\n  any required backup first (`[SB-OPS-7]`).\n- `--vacuum` - Remove claimed messages and exit\n- `--compact` - With `--vacuum`, also run SQLite VACUUM to reclaim disk space\n- `--status` - Show global message count, last timestamp, and DB size (`--status --json` for JSON output)\n- `--version` - Show version information\n- `--help` - Show help message\n\n### Commands\n\n| Command | Description |\n|---------|-------------|\n| `write <queue> [message\\|-] [--keep-newest N]` | Add a message; optional `--keep-newest` atomically claims older pending messages after the write; `-t`/`--json` print the new ID |\n| `read <queue> [options]` | Remove and return message(s) |\n| `peek <queue> [options]` | Return message(s) without removing |\n| `move <source> <dest> [options]` | Atomically transfer messages between queues |\n| `rename <old> <new> [--json]` | Retag all existing messages from one queue name to another |\n| `exists <queue> [--json]` | Check whether a queue has any messages, including claimed rows |\n| `stats <queue> [--json]` | Show pending, claimed, and total counts for one queue |\n| `list [--stats] [--prefix PREFIX \\| --pattern GLOB] [--json]` | Show queue names; `--stats` adds counts |\n| `delete <queue> [-m <id>]` | Delete a queue immediately, or physically delete a specific message by ID |\n| `delete --all` | Delete all queues immediately |\n| `broadcast [--pattern GLOB \\| --queue QUEUE ...] <message\\|->` | Send one message atomically to all existing queues, matching existing queues, or a repeatable exact set of existing literal queue names |\n| `watch <queue> [options]` | Watch queue for new messages |\n| `alias <add\\|remove\\|list>` | Manage queue aliases |\n| `dump [--include <glob>] [--exclude <glob>]` | Write queues to stdout as `simplebroker-dump` v1 ndjson (pending only, deterministic; globs on queue names; aliases match name or target; exclude wins) — `[SB-IO-*]` |\n| `load` | Restore a dump from stdin into a **fresh** broker (duplicate ids fail loudly); exit codes 0/1 — `[SB-IO-4]` |\n| `init` | Initialize SimpleBroker database in current directory (does not accept `-d` or `-f` flags) |\n\n`read`, `peek`, `move`, `dump`, and `watch` treat a downstream stdout consumer\nclosing its pipe as a clean shutdown, including exact-message and `--all`\nforms. Effects completed before the failed output remain completed. See\n[Pipe behavior](#pipe-behavior).\n\n#### Queue Aliases\n\nNormative: `docs/specs/17-ops.md` `[SB-OPS-5]`.\n\nUse aliases when two agents refer to the same underlying queue with different names. Aliases are stored in the database, persist across processes, and update atomically.\n\n```bash\n$ broker alias add task1.outbox agent1-to-agent2\n$ broker alias add task2.inbox agent1-to-agent2\n$ broker write @task1.outbox \"Job ready\"\n$ broker read @task2.inbox\nJob ready\n$ broker alias list\ntask1.outbox -> agent1-to-agent2\ntask2.inbox -> agent1-to-agent2\n$ broker alias list --target agent1-to-agent2\ntask1.outbox -> agent1-to-agent2\ntask2.inbox -> agent1-to-agent2\n$ broker write task1.outbox \"goes to literal queue\"\n$ broker read task1.outbox\ngoes to literal queue\n$ broker alias remove task1.outbox\n```\n\n- Plain queue names (`task1.outbox`) always refer to the literal queue. Use the\n  `@` prefix (`@task1.outbox`) to opt into alias resolution—if the alias is not\n  defined the command fails.\n- Alias names are plain queue names (no `@` prefix); when *using* an alias on the CLI, prefix it with `@`.\n- Use `alias list --target <queue>` to see which aliases point to a specific queue (reverse lookup).\n- Alias names and targets use the normal queue-name syntax. The target is a\n  canonical queue name, but it need not contain messages yet. New aliases must\n  remain flat: neither creation order may make an alias point to another\n  alias. Conflicting concurrent additions yield one winner.\n- Legacy invalid alias rows remain listable, one-hop resolvable, and removable;\n  SimpleBroker does not rewrite or recursively resolve them.\n- Removing an alias does not affect stored messages; they remain under the canonical queue name.\n- `rename` accepts `@alias` operands on the CLI and records canonical queue\n  names in JSON output. The Python API uses literal queue names only.\n\n### Command Options\n\n**Common options for read/peek/move:**\n- `--all` - Process all messages (CLI moves up to 1,000,000 per invocation; rerun for larger queues or use the Python API generators)\n- `--json` - Output as line-delimited JSON (includes timestamps)\n- `-t, --timestamps` - Include timestamps in output\n- `-m <id>` - Target a specific message by its exact 19-digit message ID\n- `--after <timestamp>` - Process messages with id **strictly greater** than\n  the bound (a filter, not a complete stream offset)\n- `--before <timestamp>` - Process messages with id **strictly less** than the\n  bound (`read`, `peek`, and `move`; not `watch`)\n- `--newest` - Select the highest eligible public message ID first. Bounds\n  filter before selection; `-m` may be combined with it; `--all` may not.\n\nNormative filter predicates, late older ids (including after `move` or exact\ninsert), and watch progress: `docs/specs/14-timestamp-selection.md`\n`[SB-SELECT-1]`–`[SB-SELECT-5]`. CLI string forms for non-exact bounds and\nnewest selection: `[SB-CLI-5]`–`[SB-CLI-6]` in `docs/specs/10-cli.md`.\n\nRead, peek, and move select the lowest eligible public message ID first by\ndefault. Ordinary generated writes therefore remain FIFO-like, but an exact\ninsert, load, or ID-preserving move can add a lower ID later. `--newest`\nselects the highest eligible ID for that bounded call. It is not a durable\nqueue mode and is unavailable on `--all`, generators, and watch.\n\n**Write options:**\n- `-t, --timestamps` - Print the new message's 19-digit timestamp ID on stdout\n- `--json` - Print `{\"timestamp\": \"<19-digit-id>\"}` for the new message (the message body\n  is not echoed back, unlike read/peek JSON)\n- `--keep-newest N` - In the same atomic operation as the write, leave pending\n  only the `N` highest public message IDs, including the new message, and mark\n  older pending messages claimed. `N` is from 1 to 9999.\n\n`--keep-newest` applies only to that write. It is not a stored queue size,\nphysical retention cap, or backpressure policy. Existing claimed rows do not\ncount toward `N`, and later ordinary writes, broadcasts, exact inserts, or\nmoves may grow the pending set again. Claimed rows remain visible to stats and\nclaimed inspection until vacuum removes them. Normative behavior:\n[`SB-DELIVERY-9`](docs/specs/11-delivery.md#write-time-pending-window-sb-delivery-9)\nand\n[`SB-CLI-7`](docs/specs/10-cli.md#write-time-pending-window-option-sb-cli-7).\n\n```bash\n# Write new state and leave only the 10 highest-ID messages pending.\n# Older pending messages become claimed; the new message counts toward 10.\n$ broker write --keep-newest 10 snapshots \"current state\"\n```\n\nPlace write options before the queue name (`broker write -t tasks \"job\"`).\nThey are also recognized after a literal message or after the stdin marker\n`-`; `--keep-newest N` and `--keep-newest=N` follow the same supported\npositions before `--`. Before an explicit `--`, any spelling registered as a\nCLI option remains an option or is rejected when it belongs to another\ncommand; it is never silently written as message data. Use `broker write tasks\n-- --json` to write the literal body `--json`. Unknown dash-leading bodies\nremain compatible as literal data, though an explicit `--` is the clearest\nform for scripts. Because `--newest` and `--keep-newest` are also registered,\nuse `broker write tasks -- --newest` or `broker broadcast -- --newest` for the\nliteral `--newest` body. Use `--` before a literal `--keep-newest` or\n`--keep-newest=<value>` body too.\n\n**Watch options:**\n- `--peek` - Monitor without consuming\n- `--move <dest>` - Continuously drain to destination queue\n- Use the global `-q` flag (`broker -q watch ...`) to suppress the startup message\n\n**Queue metadata options:**\n- `stats <queue>` reports counts for exactly one queue without scanning all queues.\n- `exists <queue>` exits `0` when the queue has any row and `2` when it has none.\n- `list` reports queue names. Use `list --stats` when you need counts.\n- `list --prefix <prefix>` uses a literal queue-name prefix.\n- `list --pattern <glob>` uses fnmatch-style matching.\n- `--json` on `exists`, `stats`, or `list` emits JSON suitable for scripts.\n\nQueues are implicit: a queue exists when at least one message row exists for\nthat name, including claimed rows. After vacuum removes claimed rows, a\nclaimed-only queue no longer exists. Normative specs: `docs/specs/17-ops.md` \n`[SB-OPS-1]`–`[SB-OPS-2]`.\n\n**Timestamp formats for `--after` and `--before`:** \n- ISO 8601: `2024-01-15T14:30:00Z` or `2024-01-15` (midnight UTC)\n- Unix seconds: `1705329000` or `1705329000s`\n- Unix milliseconds: `1705329000000ms`\n- Unix nanoseconds/Native hybrid: `1837025672140161024` or `1837025672140161024ns`\n\nFractional seconds are not supported in ISO, bare numeric, or suffixed numeric\nbounds. Use integer `ms`, integer `ns`, or a native hybrid message ID when you\nneed finer granularity than seconds. Numeric spellings contain decimal digits\nonly; signs and separators such as `_` are rejected. Unicode decimal digits are\naccepted and normalized before parsing.\n\n**Best practice:** Heuristics distinguish bare numeric values for interactive\nuse, but explicit suffixes (`s`/`ms`/`ns`) are recommended when a particular\nunit is intended.\n\n`--after` and `--before` use strict open bounds. Combined together, they select\nmessages where `after_timestamp < message_timestamp < before_timestamp`.\n\n`-m` / `--message` is stricter than `--after` and `--before`: it accepts only\nthe exact 19-digit broker message ID. A malformed `-m` value prints\n`invalid message ID: expected exactly 19 digits within range` on stderr and\nexits `1`. A well-formed ID that does not match a message is silent and exits\n`2`.\n\nNormative specifications: see `[SB-ID-4]`, `[SB-CLI-5]`\n(ISO, date-only UTC midnight, Unix s/ms/ns, native hybrid; suffixes\nrecommended). Bounds are strict open intervals after parse\n(`[SB-SELECT-1]`).\n\n### Exit Codes\n- `0` - Success\n- `1` - General error (e.g., database access error, invalid arguments)\n- `2` - Queue empty or no matching messages\n- `130` - An unhandled keyboard interrupt reached the outer CLI wrapper\n\nNormative specifications: `docs/specs/10-cli.md` ([SB-CLI-1]–[SB-CLI-5]).\n\n`watch` exits `0` when stopped by SIGINT/SIGTERM or when its stdout consumer\ncloses the pipe (see [Pipe behavior](#pipe-behavior)). Other commands return\n`130` when a `KeyboardInterrupt` escapes to the process wrapper; effects\ncompleted before the interrupt are not rolled back.\n\n**Note:** `delete <queue>`, `delete --all`, and `delete <queue> -m <id>` remove\nmatching rows immediately (`[SB-OPS-3]`). Reads still use claimed-row semantics\nand are reclaimed by `--vacuum` (`[SB-OPS-6]`).\n\n## Critical Safety Notes\n\n### Safe Message Handling\n\n`write --keep-newest N` is destructive to delivery state. It can make older\npending messages unavailable to ordinary consumers even though no consumer\nreceived them. Use it only when newer messages supersede older ones, and only\non a dedicated single-producer queue. On a shared queue it claims other\nproducers' pending messages. The operation claims rather than physically\ndeletes displaced rows, but automatic or explicit vacuum may later remove\nthem.\n\nMessages can contain any characters including newlines, control characters, and shell metacharacters:\n- **Shell injection risks** - When piping output to shell commands, malicious message content could execute unintended commands\n- **Special characters** - Messages containing newlines or other special characters can break shell pipelines that expect single-line output\n- **Queue names** - Limited to alphanumeric + underscore/hyphen/period (cannot start with hyphen or period)\n- **Message size** - Limited to 10MB by default; override with `BROKER_MAX_MESSAGE_SIZE`\n- **NUL characters** - Message bodies are UTF-8 text. The Postgres backend cannot store a raw NUL (`\\x00`) character: `write()` raises `OperationalError` there, while SQLite and Redis round-trip it. JSON-encoded payloads are unaffected (compliant serializers escape NUL as `\\u0000`), but avoid casting bodies to `jsonb` in sidecar queries: jsonb rejects `\\u0000` even escaped.\n\n**Always use `--json` for safe handling** - see examples below.\n\n### Robust message handling with `watch`\n\nWhen using `watch` in its default consuming mode, messages are\n**permanently removed** from the queue *before* your script or handler\nprocesses them. If your script fails or crashes, **the message is lost**.\nFor critical work, prefer atomically moving each message to an inflight queue,\nthen deleting it there after successful processing. Peek-then-delete is not a\nreservation: it is safe only for a single consumer or when duplicate handling\nis idempotent. `peek --all` and `Queue.peek_generator()` traverse live pages\nin ascending public-message-ID order. Deleting or moving previously returned\nrows out of the source does not skip later eligible rows. Older IDs inserted\nor moved behind the cursor may be missed; an empty or short page ends the\nscan. Peek remains observation, not a snapshot or an exclusive consumer\nprotocol.\n\nPython callers receive a package-root `CloseableIterator` from Queue read,\npeek, move, and stream generators and from the high-level all-message views.\nCreating one is lazy. Create, advance, exhaust, and close read, move, and stream\niterators on one thread. Peek establishes its owner thread on first\nadvancement; later use and close stay there. Close before closing the Queue or\nhigher-level client; use `contextlib.closing()` when a loop may stop early.\nClosing ends the iterator-owned Queue operation. It does not alter the selected\ndelivery settlement; for peek, it does not claim messages or turn the live\ntraversal into a snapshot.\n\nNormative delivery contract:\n`docs/specs/11-delivery.md` ([SB-DELIVERY-1]–[SB-DELIVERY-9]).\n\nSingle-consumer example: [`examples/safe_worker.sh`](https://github.com/VanL/simplebroker/blob/main/examples/safe_worker.sh)\npolls one message at a time and acknowledges it by deleting its exact ID only\nafter the configured `PROCESS_TASK` command succeeds. It exits on processing,\nacknowledgement, parse, or broker failure, so the current process cannot advance\npast a failed message. The Bash example preserves trailing newlines but rejects\nNUL payloads, which shell variables cannot represent. JSON message IDs are\nexact 19-digit strings, and the script validates that shape before streaming\nbodies to the handler's standard input. For concurrent workers, use the\nmove-to-inflight recipe in\n[`docs/agent-kernel.md`](https://github.com/VanL/simplebroker/blob/main/docs/agent-kernel.md).\n\n## Core Concepts\n\n### Timestamps as Message IDs\nEvery stored message has a public integer message ID. Python and storage keep\nthat integer domain; SimpleBroker-owned JSON exposes it as an exact 19-digit\nASCII decimal string (usually in `timestamp`). Broker-generated IDs include a\nphysical time component, but caller-supplied exact IDs need not represent\ncreation time. Timestamps can be included in regular output by passing the\n`-t` / `--timestamps` flag.\n\nBuilt-in SimpleBroker JSON already uses the canonical string. When JSON owned\nby your application includes a broker message ID or high-water value returned\nby the Python API, format that field through the public package-root helper:\n\n```python\nimport json\n\nfrom simplebroker import format_message_id\n\nmessage_id = 1234567890123456789  # e.g. returned by Queue.write()\ndocument = json.dumps(\n    {\n        \"source_message_id\": format_message_id(message_id),\n    }\n)\n```\n\nThe application-owned `source_message_id` name is illustrative. Convert only\nknown broker identity fields in JSON you construct; do not rewrite message\nbodies or unrelated application timestamps. See the\n[Python embedding guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md#serializing-message-ids-in-application-json)\nfor the full boundary rules.\n\nStored message IDs are:\n- **Unique within a backend** - No two stored rows share an ID.\n- **Monotonic when broker-generated** - Exact insertion may add a smaller ID\n  later, and `move` preserves an existing ID.\n- **Efficient** - They are 64-bit integers, not UUIDs.\n- **Time-related when broker-generated** - The physical component reflects\n  generation time within the encoding grain.\n\nMessage bodies are payload only and may duplicate byte-for-byte. Message IDs are\nthe sole durable identity for targeted broker operations and application-level\ndeduplication. Because vacuum physically removes claimed rows, SimpleBroker does\nnot retain a permanent tombstone for every historical ID; consumers that require\nidempotency should persist and deduplicate by message ID.\n\nBroker-generated message IDs are positive and equal generation time within\nthe encoding grain (~4 µs). Exact insertion via the API may supply caller-chosen\nIDs except `0`, which is reserved as a lower-bound and empty-high-water origin.\nExact selectors accept zero so legacy rows can be inspected and cleaned up.\nThe `move` operation preserves IDs.\n\nThe value `queue.last_ts` is a broker-global high-water cache, not the identity\nof the last write or a record of the broker's last activity.\n\nThe format:\n- High 52 bits: physical component from `time.time_ns()`, aligned to 4096 ns\n  steps rather than counted in microseconds.\n- Low 12 bits: logical counter.\n- The format is compatible with nanosecond Unix time, but the effective time\n  grain is ~4 µs (4096 ns). The physical component is not a microsecond counter.\n\nPython APIs that target one exact message ID, such as\n`Queue.read(message_id=...)`, `Queue.peek(message_id=...)`,\n`Queue.move(message_id=...)`, `Queue.delete(message_id=...)`,\n`Queue.delete_many(...)`, and exact-ID granular methods, accept either an\ninteger ID or an exact 19-digit string ID. Malformed string IDs raise\n`ValueError`; unsupported types, including `bool`, raise `TypeError`.\n`Queue.delete()` with no argument is the intentional queue-wide delete and\nreturns whether it removed anything. `Queue.delete(message_id=None)` is\nambiguous and raises `TypeError` before storage mutation; pass a concrete ID\nfor targeted deletion.\n\nHigh-level `Queue.move()` returns an ordinary dictionary described by the\npackage-root `MovedMessage` `TypedDict`, with `message: str` and\n`timestamp: int`. `all_messages=True` returns an iterator of the same\ndictionaries. The granular move methods retain their string/tuple shapes.\n\nID representation, allocation, write returns, high-water/cache meaning,\nexact-ID forms, and ID-preserving move are normative in the\n[message identity contract](https://github.com/VanL/simplebroker/blob/main/docs/specs/13-message-identity.md)\n`[SB-ID-1]` through `[SB-ID-5]`.\n\nUniqueness is the ordinary coexistence rule for stored rows. Applications\nneeding durable idempotency persist the message ID themselves.\n\nExact-ID Python operations accept an integer ID or a string of exactly\n19 decimal digits (`[SB-ID-4]`). Surrounding whitespace is stripped, and\n\"decimal digit\" is Python's `str.isdecimal()`, so non-ASCII decimal digits\nare accepted. Python `after_timestamp` / `before_timestamp` remain\ninteger bounds; CLI date and unit-suffix parsing apply only to CLI range\nflags.\n\n\n### JSON for Safe Processing\n\nMessages with newlines or special characters can break shell pipelines. Use\n`--json` to avoid shell issues. JSON message IDs are strings, so `jq -r`\nextracts their exact digit text without numeric conversion.\nLoud plain `read`, `peek`, `move`, and `watch` output warns once when any\nemitted body contains an embedded newline, including exact, range, all, and\ntimestamped selections. `--quiet` suppresses that commentary; JSON output\nnever emits it.\n\n```bash\n# Problem: newlines break line counting\n$ printf 'ERROR: Database connection failed\\nRetrying in 5 seconds...' | broker write alerts -\n$ broker read alerts | wc -l\n2  # Wrong! One message counted as two\n\n# Solution: JSON output (line-delimited)\n$ printf 'ERROR: Database connection failed\\nRetrying in 5 seconds...' | broker write alerts -\n$ broker read alerts --json\n{\"message\": \"ERROR: Database connection failed\\nRetrying in 5 seconds...\", \"timestamp\": \"1837025672140161024\"}\n\n# Parse safely with jq\n$ broker read alerts --json | jq -r '.message'\nERROR: Database connection failed\nRetrying in 5 seconds...\n```\n\n### Filtering by message id (`--after` / `--before`)\n\n`--after` / `--before` and the corresponding API-level `after_timestamp` and \n`before_timestamp` arguments are integer bounds used for selection only. ID `0` is \nused as the lower-bound origin so rows moved into a queue or inserted via the API can\nbe inspected and cleaned up. \n\nNormative specifications: `[SB-SELECT-2]` and (`[SB-SELECT-3]`. Full rules:\n`docs/specs/14-timestamp-selection.md`.\n\n```bash\n# Continue after a previously seen id\n$ result=$(broker read tasks --json)\n$ last=$(echo \"$result\" | jq -r '.timestamp')\n$ broker read tasks --all --after \"$last\"\n\n# Human-readable bound (CLI string forms: [SB-CLI-5])\n$ broker read tasks --all --after \"2024-01-15T14:30:00Z\"\n\n# Open interval\n$ broker peek tasks --all --after \"$start\" --before \"$end\"\n\n# Inspection only: include claimed rows not yet vacuumed\n$ broker peek tasks --all --include-claimed\n```\n\nClaimed rows are deletion-pending — vacuum may remove them at any time;\n`--include-claimed` is an inspection tool, not delivery state.\n\n```bash\n# Back up, restore, or migrate between backends (normative: [SB-IO-*]).\n# Pending-only bounded dump; load is mutating and intended for a FRESH broker.\n# The header bounds message IDs and restores broker-global allocation high-water.\n# Future clock skew warns; excessive skew refuses before mutation unless forced.\n$ broker dump > backup.ndjson\n$ broker dump --include 'tasks*' --exclude 'tasks_tmp' | (cd /fresh/dir && broker load)\n$ broker dump | BROKER_BACKEND=postgres BROKER_BACKEND_TARGET=\"$DSN\" broker load\n# Explicit recovery escape hatch; still warns and may impair writes until catch-up.\n$ broker load --force < future-watermark-backup.ndjson\n```\n\nFull dump/load and claimed-row inspection rules:\n`docs/specs/15-persistence-io.md`.\n\n## Common Patterns\n\n<details>\n<summary>Basic Worker Loop</summary>\n\n```bash\nwhile msg=$(broker read work 2>/dev/null); do\n    echo \"Processing: $msg\"\n    # do work...\ndone\n```\n</details>\n\n<details>\n<summary>Fan-out with Broadcast</summary>\n\n```bash\n# Send to all queues at once\n$ broker broadcast \"shutdown signal\"\n\n# Send to an exact existing set\n$ broker broadcast \\\n    --queue worker1 \\\n    --queue worker2 \\\n    \"targeted shutdown\"\n\n# Each worker reads from its own queue\n$ broker read worker1  # -> \"shutdown signal\"\n$ broker read worker2  # -> \"shutdown signal\"\n```\n\nBroadcast can target all existing queues, names matching a pattern, or an\nexact set of literal queue names. Python callers may explicitly create missing\nexact targets. Selection, validation, result counts, queue-creation policy,\natomicity, CLI behavior, and backend compatibility are normative in the\n[broadcast contract](https://github.com/VanL/simplebroker/blob/main/docs/specs/12-broadcast.md) `[SB-BCAST-1]`\nthrough `[SB-BCAST-6]`.\n\nBroadcast is queue fan-out, not pub/sub: it inserts ordinary pending messages\ninto the selected queues. Aliases are not targets, and CLI broadcast never\ncreates queues.\n</details>\n\n<details>\n<summary>Unix Tool Integration</summary>\n\n```bash\n# Pipe command output into a queue\n$ df -h | broker write monitoring -\n$ broker peek monitoring\n\n# Process files through a queue\n$ find . -name \"*.log\" | while read f; do\n    broker write logfiles \"$f\"\ndone\n\n# Parallel processing with xargs\n$ broker read logfiles --all | xargs -P 4 -I {} process_log {}\n\n# Remote queue via SSH\n$ echo \"remote task\" | ssh server \"cd /app && broker write tasks -\"\n$ ssh server \"cd /app && broker read tasks\"\n\n# Use absolute paths for databases in specific locations\n$ broker -f /var/lib/myapp/queue.db write tasks \"backup database\"\n$ broker -f /var/lib/myapp/queue.db read tasks\n\n# Reserving work with an atomic move: see the move-to-inflight recipe\n# in docs/agent-kernel.md (safe under concurrent workers).\n\n# broker move --all --json emits ndjson: one JSON object per line\n$ broker move todo in-process --all --json | while IFS= read -r msg_json; do\n    msg_id=$(echo \"$msg_json\" | jq -r '.timestamp')\n    msg_data=$(echo \"$msg_json\" | jq -r '.message')\n    process_message \"$msg_data\" && broker delete in-process -m \"$msg_id\"\ndone\n```\n</details>\n\n<details>\n<summary>Dead Letter Queue Pattern</summary>\n\n```bash\n# Single-consumer peek-and-ack loop. process_task_json reads one JSON object\n# from stdin; successful processing is acknowledged by exact-id deletion.\nwhile true; do\n    if msg_json=$(broker peek tasks --json); then\n        :\n    else\n        rc=$?\n        [ \"$rc\" -eq 2 ] && break  # queue is empty\n        exit \"$rc\"                # broker failure is fatal\n    fi\n\n    # Validate the exact string token before passing it back to the CLI.\n    msg_id=$(printf '%s\\n' \"$msg_json\" | python3 -c \\\n        'import json, sys; v=json.load(sys.stdin)[\"timestamp\"]; (type(v) is str and len(v) == 19 and v.isascii() and v.isdecimal() and int(v) < 2**63) or sys.exit(\"invalid message ID\"); print(v)') || exit 1\n\n    if printf '%s\\n' \"$msg_json\" | process_task_json; then\n        broker delete tasks -m \"$msg_id\" >/dev/null || exit 1\n    else\n        # Exact-id move is atomic: a failed move leaves the source pending.\n        broker move tasks dlq -m \"$msg_id\" >/dev/null || exit 1\n    fi\ndone\n```\n\nAfter inspecting the dead-letter queue, retry its messages manually:\n\n```bash\n# Retry failed messages\nbroker move dlq tasks --all\n```\n</details>\n\nFor a checkpointing worker with atomic checkpoint updates and\nper-message acknowledge-by-delete, explicit empty-queue exit handling, and\nfatal operational-error handling, see\n[`examples/resilient_worker.sh`](https://github.com/VanL/simplebroker/blob/main/examples/resilient_worker.sh).\nSet `PROCESS_EVENT` to one executable command/path to replace its demonstration\nhandler; the message is streamed to the command's standard input. Its checkpoint\nis an informational record of the last acknowledgement, not a `--after` filter,\nbecause older pending IDs can arrive later through exact insertion or move.\n\n## Real-time Queue Watching\n\nThe `watch` command provides three modes for monitoring queues:\n\n1. **Consume** (default): Process and remove messages from the queue\n2. **Peek** (`--peek`): Monitor messages without removing them\n3. **Move** (`--move DEST`): Continuously drain ALL messages to another queue\n\n```bash\n# Start watching a queue (consumes messages)\n$ broker watch tasks\n\n# Watch without consuming (peek mode)\n$ broker watch tasks --peek\n\n# Watch with JSON output (timestamps always included)\n$ broker watch tasks --json\n{\"message\": \"task 1\", \"timestamp\": \"1837025672140161024\"}\n\n# Continuously drain one queue to another\n$ broker watch source_queue --move destination_queue\n```\n\nMove mode drains the entire source queue with one atomic move per message\nand is incompatible with `--after`/`--before` filters (they would leave\nmessages stranded). Watch behavior is normative in\n`docs/specs/11-delivery.md` (`[SB-DELIVERY-2]`, `[SB-DELIVERY-3]`); the\npolling strategy and move-mode details are in the\n[Python guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md#watchers-in-depth).\n\n### Pipe behavior\n\nWhen the process consuming SimpleBroker's stdout exits (for example,\n`broker watch q | head -1`), SimpleBroker stops at its next delivery attempt\nand exits `0`. An idle watcher does not learn the pipe closed until it next\ntries to write to it. With the default consume semantics, the message whose\ndelivery detected the closed pipe was already claimed and is not returned to\nthe queue; no further messages are claimed. A configured at-least-once\n`read --all` batch instead rolls back its still-uncommitted batch when the\nstream closes. Exact reads and moves may have completed their atomic claim or\nmove before output detects closure; `move --all` completes its selected atomic\nmoves before printing them. Those completed effects are not reversed.\n\nExit `0` means SimpleBroker shut down cleanly. It does not validate that the\nconsumer processed any particular message; check the consumer's own exit\nstatus.\n\nThat clean-stop rule is limited to `read`, `peek`, `move`, `dump`, and\n`watch`. If a downstream consumer closes while a finite result such as help,\nversion, `list`, `stats`, status, or an opted-in write/rename result is being\ndelivered, SimpleBroker returns `1` with a controlled stderr diagnostic. A\nwrite or rename may already be durable; inspect broker state before retrying.\n\n### Delivery guarantees\n\nDefault operations claim atomically: the claim commits before your code\nsees the message — the consume claim boundary, `[SB-DELIVERY-1]`. That is\nbroker delivery state, not proof of application processing\n(see [Critical Safety Notes](#critical-safety-notes)). Generator APIs\naccept `delivery_guarantee=\"at_least_once\"` (`[SB-DELIVERY-5]`) for\nretry-on-stop batch processing. Queue read, peek, move, and stream iterators\nreturn `CloseableIterator`; they are lazy and thread-affine in every mode.\nRead, move, and stream stay on their creation thread. Peek establishes its\nowner on first advancement. Early-stop callers must close on the owning thread\nbefore their Queue/client.\n\nSpecifications: `docs/specs/11-delivery.md`\n(`[SB-DELIVERY-1]`–`[SB-DELIVERY-9]`); worked patterns, generator rules,\nand the cross-thread safety net are in the\n[Python guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md#delivery-guarantees-in-practice).\n\n### Queue metadata\n\nUse targeted metadata APIs when you need queue existence or counts:\n\n```python\nfrom simplebroker import Queue, QueueStats\n\nqueue = Queue(\"tasks\")\n\nif queue.exists():\n    stats: QueueStats = queue.stats()\n    print(stats.pending, stats.claimed, stats.total)\n```\n\n`QueueStats.pending` is the unclaimed count. `QueueStats.claimed` is the count\nof deletion-pending messages claimed by a consume operation or by write-time\n`keep_newest`, but not yet vacuumed. `QueueStats.exists` is true when\n`total > 0`.\n\nFor cross-queue metadata, `open_broker(...).list_queues()` returns queue names\nonly, including claimed-only queues. Use `list_queue_stats()` when you need\ncounts.\n\nSpecifications: `docs/specs/17-ops.md` `[SB-OPS-1]`–`[SB-OPS-2]`.\n\n### Watching from Python\n\n```python\nfrom simplebroker import QueueWatcher\n\n\ndef handle_message(msg: str, ts: int):\n    print(f\"Processing: {msg}\")\n\n\nwatcher = QueueWatcher(\"orders\", handle_message, db=\"my.db\")\nthread = watcher.run_in_thread()  # background thread; watcher.stop() to end\n```\n\nWatchers also work as context managers and support peek mode, error\nhandlers, and clean shutdown via `StopWatching`. Details:\n[Python guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md#watchers-in-depth).\n\n### Going deeper\n\nThe [Python guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md)\ncovers the rest of the API surface:\n\n- delivery guarantees in practice, generators, and thread rules\n- async integration and thread-based background processing\n- cross-queue operations with `open_broker()` (broadcast, rename,\n  `delete_from_queues`, `find_message_ids`)\n- exact-ID workflows: `generate_timestamp()`, `insert_messages()`,\n  `latest_pending_timestamp()`, high-water tracking (`[SB-ID-2]`–`[SB-ID-4]`)\n- sidecar tables (`[SB-API-7]`), the reactor pattern, and activity waiters\n- backend identity and PostgreSQL connection-pressure inspection\n  (`[SB-API-3]`, `[SB-API-13]`–`[SB-API-15]`), including process owner\n  labels and coherent total/by-owner usage\n- custom extensions (`BrokerCore` / `BrokerDB` boundaries)\n\n## Embedding SimpleBroker in Your Project\n\nFor embedded use, put a small project-level client or context object in\nfront of SimpleBroker: resolve the broker target once, translate your\napplication’s namespaced settings into a Config, and hand out queues bound to\nthat target. The library does not read `BROKER_*` environment variables on\nits own; call `resolve_config(env=os.environ)` once at startup to honor them, or\n`resolve_config()` for defaults only. Pass the read-only `Config` to every\nhandle. Weft is the reference implementation of the client shape.\n\nFor a worker that reuses backend resources across several queues, hold one\n`BrokerSession` for that worker's task scope. Exiting on the worker thread\ncloses the queues it minted, releases that thread's cached core, and drops the\nsession lease:\n\n```python\nfrom simplebroker import BrokerSession\n\nwith BrokerSession.connect(target, config=config) as session:\n    session.queue(\"jobs\").write(\"render invoice\")\n```\n\nThe full pattern — client shape, configuration snapshots, redaction rules,\nand the `simplebroker.commands` command layer (`[SB-API-10]`, the\nprogrammatic CLI equivalent) — is in the\n[Python guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md#embedding-simplebroker-in-your-project).\n\n## Performance & Tuning\n\n[`bin/benchmark.py`](bin/benchmark.py) records a best-of-three\noperations/second matrix across workload, access type, and backend. This M4\nMacBook Pro run used 100 operations per sample, 100-byte messages, automatic\nvacuum disabled, and all other SimpleBroker settings at their defaults:\n\n| Backend | Access | Writes | Reads | Peeks | Mixed |\n|---------|--------|-------:|------:|------:|------:|\n| `sqlite` | `cli` | 14.7 | 12.4 | 12.5 | 13.1 |\n| `sqlite` | `api` | 909.1 | 898.2 | 1,139.8 | 962.8 |\n| `sqlite` | `optimized-api` | 7,125.2 | 5,945.4 | 22,314.6 | 7,879.6 |\n| `pg` | `cli` | 6.3 | 5.8 | 5.9 | 6.1 |\n| `pg` | `api` | 105.7 | 113.1 | 130.9 | 102.5 |\n| `pg` | `optimized-api` | 767.5 | 1,039.8 | 4,423.3 | 1,402.0 |\n| `redis` | `cli` | 8.0 | 7.9 | 8.0 | 8.0 |\n| `redis` | `api` | 250.2 | 263.0 | 205.7 | 145.8 |\n| `redis` | `optimized-api` | 2,145.0 | 5,252.6 | 3,934.5 | 3,700.5 |\n\n`cli` includes a fresh Python process for every operation. `api` uses the\ndefault ephemeral connection behavior. `optimized-api` runs the same calls\nwith `persistent=True`. These are point-in-time measurements from a short\nlocal run, not performance guarantees; topology, hardware, load, and software\nversions will change the result. The attributable raw measurements are in the\n[result artifact](benchmarks/results/2026-08-10-m4-matrix.json). Use `--all`\nfor bulk operations where its\ndelivery semantics fit the workload.\n\nFor normal use in the embedding or shell-tool context, SimpleBroker is\nunlikely to be the bottleneck: the processes it coordinates typically take\nmilliseconds to minutes per work item. Full numbers and tuning guidance:\n[configuration guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/configuration.md#performance-and-tuning).\n\n### Cross-Backend Benchmarking\n\nThe repository benchmark measures CLI, API, and optimized API access across\nSQLite, Postgres, and Redis. Reproduction commands and workload definitions:\n[backends guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/backends.md#cross-backend-benchmarking).\nAn opt-in `--sqlite-tuning` argument appends a separate SQLite setting\nsensitivity table without expanding the default matrix.\n\n### Environment Variables\n\nMost users will not need to adjust any settings. The `broker` command reads\nthese variables at startup; a Python program honors them by passing\n`resolve_config(env=os.environ)` to its handles. If tuning is desired, the most\nlikely settings will be:\n\n| Variable | Default | Purpose |\n|----------|---------|---------|\n| `BROKER_BUSY_TIMEOUT` | `5000` | SQLite busy timeout (ms) |\n| `BROKER_SYNC_MODE` | `FULL` | Durability mode; `NORMAL` can improve write throughput with a small power-loss risk, so benchmark the tradeoff on your workload |\n| `BROKER_READ_COMMIT_INTERVAL` | `1` | Messages per commit in `--all` mode; `1` keeps the per-message claim boundary (`[SB-DELIVERY-1]`), higher values batch with at-least-once semantics (`[SB-DELIVERY-5]`) |\n| `BROKER_DEFAULT_DB_NAME` | `.broker.db` | Database filename (all scopes) |\n| `BROKER_PROJECT_SCOPE` | unset | Enable git-like upward project discovery |\n| `BROKER_MAX_MESSAGE_SIZE` | 10MB | Maximum message body size |\n\nThe full settings catalog — vacuum and watcher tuning, database and\nconfig naming, and the rest of the documented keys — is in the\n[configuration guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/configuration.md).\n\n## Project Scoping\n\nBy default, each directory gets its own isolated `.broker.db`, and a\n`.broker.toml` in the current directory is honored. Two more modes cover\nshared use:\n\n- **Project scope** (`BROKER_PROJECT_SCOPE=true`): git-like upward search\n  for a shared project config or database. Requires explicit\n  `broker init`; SimpleBroker never creates databases in unexpected\n  locations.\n- **Global scope** (`BROKER_DEFAULT_DB_LOCATION=/abs/path`): one fixed\n  location for all broker operations.\n\n```bash\ncd /project && broker init\nexport BROKER_PROJECT_SCOPE=true\ncd /project/anywhere/below\nbroker write build-tasks \"compile assets\"   # shares /project/.broker.db\n```\n\nDiscovery precedence, database and config naming, boundary and trust\nrules, and security notes:\n[configuration guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/configuration.md).\nFor use by more than one OS user, that guide defines the required effective\npermissions on the SQLite database, all companion files, and their containing\ndirectory. SimpleBroker does not provision or preserve a cross-user sharing\npolicy.\nDo not put SQLite databases on network filesystems; use the Postgres or\nRedis backends for multi-host access.\n\n\n## Development & Contributing\n\nDevelopment setup, the test harness, lint and type checks, and the\nrelease procedure are in\n[CONTRIBUTING.md](https://github.com/VanL/simplebroker/blob/main/CONTRIBUTING.md).\nKeep it simple, maintain backward compatibility, add tests, and update\ndocumentation.\n\n\n## Going Further\n\n| Need | Where |\n|------|-------|\n| Agent-oriented use and embedding kernel | [docs/agent-kernel.md](https://github.com/VanL/simplebroker/blob/main/docs/agent-kernel.md) |\n| Advanced Python API, embedding, sidecar, reactor | [Python guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/python.md) |\n| Full configuration, scoping, tuning, security | [Configuration guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/configuration.md) |\n| Postgres / Redis backends and backend authoring | [Backends guide](https://github.com/VanL/simplebroker/blob/main/docs/guides/backends.md) |\n| Exact behavior contracts (`[SB-*]` codes) | [docs/specs/](https://github.com/VanL/simplebroker/blob/main/docs/specs/00-specs-index.md) — CLI `docs/specs/10-cli.md`, delivery `docs/specs/11-delivery.md`, broadcast `docs/specs/12-broadcast.md`, identity `docs/specs/13-message-identity.md`, selection `docs/specs/14-timestamp-selection.md`, dump/load `docs/specs/15-persistence-io.md`, library `docs/specs/16-python-library-api.md`, operations `docs/specs/17-ops.md` |\n| Runnable examples (workers, DLQ, migration, reactor) | [examples/](https://github.com/VanL/simplebroker/blob/main/examples/README.md) |\n| Storage internals and design rationale | [docs/implementation/](https://github.com/VanL/simplebroker/blob/main/docs/implementation/00-implementation-index.md) |\n| Behavior changes by release | [CHANGELOG.md](https://github.com/VanL/simplebroker/blob/main/CHANGELOG.md) |\n| Contributing and releases | [CONTRIBUTING.md](https://github.com/VanL/simplebroker/blob/main/CONTRIBUTING.md) |\n\n\n## License\n\nMIT © Van Lindberg\n\n## Acknowledgments\n\nBuilt with Python, SQLite, and the Unix philosophy.\n"}