Unified cross-platform connection manager for a single agent's persistent brain.

Uses the StorageAdapter interface from @framers/sql-storage-adapter to support multiple backends (better-sqlite3, sql.js, IndexedDB, Postgres, etc.) transparently. All methods are async.

Usage:

const brain = await Brain.open('/path/to/agent/brain.sqlite');

// Async query API for subsystems
const row = await brain.get<{ value: string }>('SELECT value FROM brain_meta WHERE key = ?', ['schema_version']);

// Meta helpers
await brain.setMeta('last_sync', Date.now().toString());
const ver = await brain.getMeta('schema_version'); // '1'

await brain.close();

Subsystems (KnowledgeGraph, MemoryGraph, ConsolidationLoop, etc.) receive the Brain instance and call its async proxy methods (run, get, all, exec, transaction) for all database operations.

Accessors

  • get brainId(): string
  • Brain identifier scoping every query through this Brain instance. Subsystems (KnowledgeGraph, MemoryGraph, ConsolidationLoop) read this to inject brain_id into their own SQL.

    Returns string

  • get adapter(): StorageAdapter
  • Expose the raw storage adapter for advanced usage.

    Code that needs the adapter itself reads it here; the brain's own queries go through run, get, all and transaction.

    Returns StorageAdapter

  • get features(): StorageFeatures
  • Platform-aware feature bundle (dialect, FTS, BLOB codec, exporter). Consumers use this to generate cross-platform SQL instead of hardcoding SQLite-specific syntax.

    Returns StorageFeatures

Methods

  • Open a Brain backed by SQLite. Tries adapters in order: better-sqlite3 (Node native) -> sql.js (WASM) -> indexeddb (browser).

    Parameters

    • dbPath: string

      File path. Use :memory: for in-process testing.

    • opts: {
          brainId?: string;
          priority?: ("better-sqlite3" | "sqljs" | "indexeddb")[];
          coordinationToken?: object;
      } = {}
      • Optional brainId?: string

        Optional explicit brainId; defaults to file basename (or 'default' for :memory:).

      • Optional priority?: ("better-sqlite3" | "sqljs" | "indexeddb")[]

        Override the default adapter priority.

      • Optional coordinationToken?: object

        Optional process-local identity for non-filesystem adapters whose resource name is adapter-specific.

    Returns Promise<Brain>

    A fully initialised Brain instance with the latest schema.

  • Open a Brain backed by PostgreSQL. Requires the pg npm package and a reachable Postgres instance.

    Every integer column of the Brain's schema is BIGINT on Postgres, and this adapter reads BIGINT as a number, as SQLite returns its integers: a trace's retrieval count, access and creation times and deleted flag load as numbers on both dialects.

    Parameters

    • connectionString: string

      Standard Postgres connection URL.

    • opts: {
          brainId: string;
          poolSize?: number;
      }
      • brainId: string

        REQUIRED. Used to scope every query so multiple brains can share one Postgres database without leaking rows.

      • Optional poolSize?: number

        pg connection pool size. Defaults to 10.

    Returns Promise<Brain>

  • Open a Brain with a pre-resolved StorageAdapter. Use when sharing an adapter across subsystems (e.g., wilds-ai foundation pool + brain) or when the consumer needs full control over adapter resolution.

    CONCURRENCY CAVEAT (Postgres adapters): the underlying PostgresAdapter tracks transactionalClient as instance-shared mutable state. Two concurrent Brain.transaction(...) calls on the same shared adapter (or any subsystem call that internally opens a transaction) will overwrite each other's connection assignment, which corrupts both transactions silently. If the consumer dispatches concurrent writes through subsystems sharing one adapter, either:

    1. construct a fresh PostgresAdapter per logical actor, OR
    2. serialize transactions at the consumer layer until the adapter gets AsyncLocalStorage-tracked transactional clients. Non-transactional concurrent reads/writes against a shared adapter are safe (the pool handles those correctly).

    INTEGER COLUMNS (Postgres adapters): the Brain's integer columns are BIGINT on Postgres, which pg reads as strings unless the adapter is opened with typeParsers: { int8: 'number' }, as Brain.openPostgres opens its own. Read as strings, a trace's retrieval count and times load as text, and an access appends to the count instead of adding to it.

    Parameters

    • adapter: StorageAdapter

      Pre-built StorageAdapter instance.

    • opts: {
          brainId?: string;
          coordinationToken?: object;
      } = {}
      • Optional brainId?: string

        Required for postgres-kind adapters; optional for sqlite-kind adapters (defaults to 'default').

      • Optional coordinationToken?: object

        Optional shared opaque token for separate adapter objects that address the same backing database and brain.

    Returns Promise<Brain>

  • Execute a mutation statement (INSERT, UPDATE, DELETE).

    Parameters

    • sql: string

      SQL statement with ? positional placeholders.

    • Optional params: StorageParameters

      Parameter array matching the placeholders.

    Returns Promise<StorageRunResult>

    Metadata about affected rows.

  • Retrieve a single row (or null if none found).

    Type Parameters

    • T = unknown

    Parameters

    • sql: string

      SQL SELECT statement.

    • Optional params: StorageParameters

      Parameter array.

    Returns Promise<null | T>

    First matching row or null.

  • Retrieve all rows matching the statement.

    Type Parameters

    • T = unknown

    Parameters

    • sql: string

      SQL SELECT statement.

    • Optional params: StorageParameters

      Parameter array.

    Returns Promise<T[]>

    Array of matching rows (empty array if none).

  • Execute a callback within a database transaction.

    The transaction is automatically committed on success or rolled back on error.

    Type Parameters

    • T

    Parameters

    • fn: ((trx) => Promise<T>)

      Async callback receiving a transactional adapter.

        • (trx): Promise<T>
        • Parameters

          • trx: StorageAdapter

          Returns Promise<T>

    Returns Promise<T>

    Result of the callback.

  • Read a value from the brain_meta key-value store.

    Parameters

    • key: string

      The metadata key to look up.

    Returns Promise<undefined | string>

    The stored string value, or undefined if the key does not exist.

  • Upsert a value into the brain_meta key-value store.

    Uses INSERT OR REPLACE semantics — creates the row if absent, or overwrites if present.

    Parameters

    • key: string

      The metadata key.

    • value: string

      The string value to store.

    Returns Promise<void>

  • Check whether a given embedding dimension is compatible with this brain.

    On first call (no stored embedding_dimensions), returns true and stores the provided dimension for future compatibility checks.

    Subsequent calls compare dimensions against the stored value. Mismatches indicate that a different embedding model was used to encode memories — mixing dimensions would corrupt vector similarity searches.

    Parameters

    • dimensions: number

      The embedding vector length to check (e.g. 1536 for OpenAI ada-002).

    Returns Promise<boolean>

    true if compatible (or no prior value), false on mismatch.

  • The statement that rebuilds this brain's full-text index from its traces, for run() on the brain or on a transaction. SQLite keeps the index in one FTS5 table per file and rebuilds it whole. Postgres keeps it in the _tsv column of the memory_traces table every brain shares, so there the statement updates this brain's rows only.

    Returns {
        sql: string;
        params: unknown[];
    }

    The SQL and its parameters.

    • sql: string
    • params: unknown[]
  • Materialize this brain to a portable SQLite file at targetPath.

    Source can be any backend (SQLite, Postgres, Capacitor, etc.); output is always a fresh SQLite file. Used by .wildsoul-style export and other portability flows.

    Refuses to overwrite an existing file at targetPath so callers do not silently lose data.

    The rows come from one consistent view of the database where it gives one, Postgres or a SQLite file in WAL mode as openSqlite opens it (see _readSnapshot), so a write that lands while the export runs is wholly in the file or wholly out of it. The brain's scope-id form record (scope_id_form in brain_meta; see scopeIdForm.ts) is read before the rows and again after them: on a brain with one connection, which reads no snapshot, a re-file that commits while the export reads makes the export copy again, so the file's record names the form its rows are in. The file is written under a temporary name beside targetPath and moved there once whole: a failed export leaves nothing at targetPath, and a retry can run. A file that appears at targetPath while the export runs is kept, and the export fails.

    Forking semantics: rows are emitted with the source brainId. Importing the resulting file under a different brainId produces a fork.

    Parameters

    • targetPath: string

      Destination file path. File must not exist.

    Returns Promise<{
        bytesWritten: number;
    }>

    Bytes written to the destination file.

    Throws

    When anything exists at targetPath, before the export or once it is written; or when the scope-id form record changed while the export read the rows, on each of three passes.

  • Load a portable SQLite file into this Brain's adapter.

    Forking semantics: rows from the source file are written under the RECEIVING brain's brainId, not the brainId stored in the source file. This means importing an alice snapshot into a Brain opened with brainId: 'alice-fork' produces a fork with no shared identity.

    CAVEAT: importing from a file an older release wrote MUTATES the source file. Opening the source via Brain.openSqlite runs its pending migrations in place: v1 to v2 for a file from before 0.3.0, and v2 to v3, which adds memory_traces.scope_id and fills it, for a file from before 0.13.159. A release before 0.13.159 then fails to import the file into a table without the column, since its import copies every column it reads. The open also files the scope id of each source trace that a release older than the column wrote. To preserve the source unchanged, copy the file to a temp path before calling this method.

    A trace's episodic copy the file holds under another node id than its trace's key (a release older than keyed copies recorded it so) is filed under the key once the rows are in, with the relations that name it, so a delete by its trace id finds it (keyTraceCopies); a copy whose key the brain's own copy of the trace holds is deleted.

    A Brain a learning layer serves holds its plain user and persona scope ids in the encoded form and records it in brain_meta (scope_id_form; see scopeIdForm.ts). A source with the record whose user and persona scope ids, in its traces and its knowledge-graph copies, hold none of /, % and : reads the same in both forms and is taken as a plain one: its record is not copied, so a Brain without the record stays without it. A plain source goes into a Brain that holds the record with its ids encoded as they are written, each trace's knowledge-graph copy with it, merged or replaced. A source with the record goes into a Brain without it, which gets the record, unless the Brain holds a plain id the record would read as another, in a trace or a copy: then nothing is written and the call throws, naming the record. A replace empties the brain first, so only its form counts. The handshake and every write share one transaction, so a re-file or another import of the brain that commits first is followed (on Postgres the import waits for the brain's form lock); the source's record is read before the copy and again after it, and the copy is made again when a re-file of the source committed in between.

    Parameters

    • sourcePath: string

      Source SQLite file path (typically produced by Brain.exportToSqlite).

    • opts: {
          strategy?: "merge" | "replace";
      } = {}
      • Optional strategy?: "merge" | "replace"

        'merge' (default) upserts on PK collision; 'replace' wipes all rows for the receiving brainId first.

    Returns Promise<{
        tablesImported: Record<string, number>;
    }>

    Counts of rows imported per table.

    Throws

    When the source holds several brain ids, or when the scope-id form handshake refuses it, naming the record (scope_id_form).

  • Close the database connection.

    Must be called when the agent shuts down to flush the WAL and release the file lock. Failing to close may leave the database in WAL mode with an unconsumed WAL file.

    Returns Promise<void>