Skip to content

Store conversation history in a database

By default, Bub persists conversation Tapes as local JSONL files.

By installing the bub-tapestore-sqlalchemy plugin, you can switch the application to SQLite, PostgreSQL, or any relational database supported by SQLAlchemy, while keeping ,tape.info, ,tape.search, session branching, and Sidecar behavior exactly the same.

  • You have installed the Bub application and configured a working model.
  • (Optional) Install the sqlite3 command line tool for querying and inspecting tables directly.
  • Confirm the current Bub home path:
    printf '%s\n' "${BUB_HOME:-$HOME/.bub}"

Install the store plugin in the environment where Bub runs:

bub install bub-tapestore-sqlalchemy@main
bub hooks

Run bub hooks to check that the extension point is mounted:

provide_tape_store: tapestore-sqlalchemy, builtin

Higher-priority implementations are listed first. Seeing tapestore-sqlalchemy means the plugin is active and will override the built-in file store.

The plugin uses <BUB_HOME>/tapes.db by default. To make demonstration and verification easier, this guide points it at the current working directory:

export BUB_TAPESTORE_SQLALCHEMY_URL="sqlite+pysqlite:///$PWD/bub-tapes.db"
  • sqlite+pysqlite uses the SQLite driver built into the Python standard library, with no extra dependencies.
  • The URL must follow standard SQLAlchemy connection string conventions; here $PWD is used to build an absolute path.
Environment variable Default Description
BUB_TAPESTORE_SQLALCHEMY_ECHO false Set to true to print executed SQL statements for development and debugging.
BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS {} JSON object passed to create_engine(connect_args=...) (for example, timeout settings).

To make the settings persist, add the variables to a .env file at the workspace root. The plugin initializes the required table schema automatically on first connection.

Run one session, then use the built-in command to check whether the Tape was written to the database:

bub run "Say hello in one sentence and indicate that this turn of the conversation is stored in the database."
bub run ",tape.info"

The output should include the Tape name, entry count, and anchor status for this session:

name: 86774b31b96845a4__0b871d5e50e7c192
entries: 9
anchors: 1
last_anchor: session/start
entries_since_last_anchor: 8

Use sqlite3 to inspect the physical records in the database:

# Query the Tape session list
sqlite3 "$PWD/bub-tapes.db" \
  "SELECT name, last_entry_id FROM tapes ORDER BY name;"
# Query anchor records
sqlite3 "$PWD/bub-tapes.db" \
  "SELECT entry_id, anchor_name, entry_date FROM tape_entries WHERE kind = 'anchor';"

The tapes table maintains metadata for all sessions; the tape_entries table records the specific content of each Turn, model answer, and tool call entry by entry.

The plugin is built on SQLAlchemy and supports connecting to relational databases such as PostgreSQL and MySQL:

  1. Install the driver (install the corresponding database driver in the environment where bub runs):
    # For example, PostgreSQL
    uv add "psycopg[binary]"
  2. Configure the connection string:
    export BUB_TAPESTORE_SQLALCHEMY_URL="postgresql+psycopg://user:YOUR_DB_PASSWORD@localhost:5432/bub_db"

In production, test concurrent writes and connection pool timeout settings in advance to keep the service stable.

core.Agent and the built-in app depend only on the store protocol, so implementing it swaps the backend entirely. For the difference between “the Tape keeps history” and “TapeContext projects context”, see Tape and model context.

Protocol Where it is used
bub.core.store.TapeStore Synchronous implementation passed to Agent(store=...)
bub.core.store.AsyncTapeStore Asynchronous implementation; recommended for databases and remote storage
provide_tape_store Application-level replacement; the built-in app returns FuzzyFileTapeStore

Full method signatures are in the Python API. At minimum a backend must isolate entries by Tape name, preserve append order, support reset(), and let TapeQuery’s anchor filters work. Forks are built on top of any store by ForkTapeStore, so the backend does not implement them.

The built-in app supplies FuzzyFileTapeStore through provide_tape_store, with ~/.bub/tapes/ as the default directory. It derives the Tape name from both workspace path and session ID, so same-named sessions in different workspaces do not share history.

BubAgent chooses its store in this order:

  1. An explicitly supplied tape_store;
  2. The store returned by framework.get_tape_store() when the Agent is created;
  3. The in-memory store when neither is available.

Create BubAgent inside framework.running() when it should use the store supplied by that scope. Replace storage with provide_tape_store, and replace the projection strategy with build_tape_context.

The built-in app restores session settings such as model, reasoning effort, and code mode from the Tape. Persist any other application state explicitly.

Common issue Possible cause Solution
tapestore-sqlalchemy is missing from bub hooks The plugin was installed into a different Python environment Check command -v bub and make sure bub install ran in the virtual environment you are using.
SQLite reports that the database is locked (Database is locked) Concurrent writes from multiple processes exceed what SQLite can handle Configure a busy timeout, or switch to a relational database better suited to high concurrency, such as PostgreSQL.
Connecting to PostgreSQL raises ModuleNotFoundError The corresponding database driver is not installed Run uv add "psycopg[binary]" in the Bub environment to install the driver.
Tables cannot be created, or permission is denied The target path or remote database user lacks permission to create tables Confirm that the file directory is writable by the current user, or grant the database user CREATE TABLE permission.

To restore Bub’s default file storage mode, clear the environment variables and delete the test database:

unset BUB_TAPESTORE_SQLALCHEMY_URL BUB_TAPESTORE_SQLALCHEMY_ECHO BUB_TAPESTORE_SQLALCHEMY_CONNECT_ARGS
rm -f "$PWD/bub-tapes.db"