Files
gbrain/docs/architecture/topologies.md
T
d0d0e2a64a v0.37.11.0: fresh-install PGLite embedding setup fix wave (#1286)
* chore(test): preload gateway to OpenAI/1536 so 1536-dim test fixtures keep working

The v0.37 fix wave changes the canonical gateway defaults to
zeroentropyai:zembed-1 / 1280 (matching what v0.36 already chose as the
system default). 20+ test files have hardcoded new Float32Array(1536)
fixtures that match the OLD schema default. Without this preload, those
tests fail with a vector-dim-mismatch on insert.

The preload is gateway-only — it doesn't change which model gbrain ships
to production users. Tests that want the new ZE/1280 defaults call
configureGateway() explicitly in their own beforeAll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai): canonical embedding defaults + sweep across schema/engines/registry

Closes the v0.36 defaults drift bug class. The gateway shipped
zeroentropyai:zembed-1 / 1280 as the system default in v0.36 but eight
other places kept hardcoding 1536 / text-embedding-3-large. Fresh
gbrain init --pglite sized the column to 1536, the embed pipeline used
ZE/1280, and every page failed with dim mismatch.

- New src/core/ai/defaults.ts leaf module is the canonical source for
  DEFAULT_EMBEDDING_MODEL / DEFAULT_EMBEDDING_DIMENSIONS. Schema and
  registry helpers import from this lean module instead of pulling the
  full gateway (which loads every provider SDK).
- src/core/ai/gateway.ts re-exports the constants for back-compat.
- src/core/pglite-schema.ts getPGLiteSchema() defaults track gateway.
- src/core/postgres-engine.ts getPostgresSchema() default args track
  gateway (same drift on the Postgres path — codex round 1 CDX-1).
- Both engine.initSchema() fallbacks track gateway constants (no more
  stale OpenAI/1536 catch-block defaults).
- Schema seed stops stripping the provider prefix; full provider:model
  is stored in the DB config table (codex round 1 CDX-4).
- Chunk-row INSERT defaults track gateway (codex round 2 CDX2-4 —
  pglite-engine:1611 + postgres-engine:1647 were production write
  sites previously hardcoded to text-embedding-3-large).
- src/core/search/embedding-column.ts loadRegistry + isCacheSafe gain
  the cfg > gateway > DEFAULT resolution chain (codex round 2 CDX2-3).
  The gateway tier matters because callers that configure the gateway
  (init paths, tests, programmatic SDK) expect the registry to mirror
  that state when cfg doesn't have an explicit embedding_model.

Tests:
- schema-templating: default expectation flips to ZE/1280 (v0.37 truth).
- embedding-dim-check: 3 new engine-kind branching cases + updated
  fresh-brain expectation (under legacy preload).
- embedding-column: registry + isCacheSafe expectations match new chain.
- v0_28_5-fix-wave E2E: engineKind required arg propagated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(init+config+cli): always-configure gateway, file-only loader, honest config-set, sync/reinit help

Closes the "fresh init doesn't work + config-set silently lies" bug
class end-to-end. Six related changes that ship together because the
file-plane/DB-plane contract only holds when init paths, config-set,
the gateway env mapping, and the recipe text all agree.

Lane B (init paths):
- initPGLite, initPostgres, initMigrateOnly always configureGateway()
  before engine.initSchema(). Pre-fix the call was gated on flags, so
  bare `gbrain init --pglite` left the gateway unconfigured and the
  engine fell through to stale OpenAI/1536 defaults instead of the
  ZE/1280 the gateway would have resolved.
- New configureGatewayWithMergedPrecedence() helper applies the locked
  precedence chain `CLI > env > existing file > gateway internal`.
- printResolvedAIChoice() shows the resolved model/dim at init time +
  surfaces a ZE setup hint inline when the API key is missing.
- B.4: saveConfig merge uses loadConfigFileOnly() so transient env
  state (DATABASE_URL, etc.) never poisons ~/.gbrain/config.json
  (codex round 2 CDX-5).
- B.5: extend the v0.28.5 dim-mismatch detector so it fires when the
  gateway-resolved dim differs from the existing column, not only
  when --embedding-dimensions is explicit (codex round 2 CDX-6).

Lane C (config plane):
- New `loadConfigFileOnly()` reads ~/.gbrain/config.json only — no env
  merge, no DATABASE_URL inference. Safe write-back source for init.
- GBrainConfig gains `zeroentropy_api_key?: string`. loadConfig merges
  process.env.ZEROENTROPY_API_KEY. buildGatewayConfig at cli.ts:1401
  maps it into env.ZEROENTROPY_API_KEY so ZE recipes finally see it
  (codex round 2 CDX2-5+6 — the v1 fix landed in the wrong file).
- `gbrain config set embedding_model` and `... embedding_dimensions`
  refuse unconditionally and print a paste-ready wipe-and-reinit
  recipe. No --force escape (codex round 2 CDX2-13).
- migrate-engine.ts adds a contract comment at the DB-plane write
  site documenting "DB stores schema-applied metadata; file plane is
  canonical for runtime gateway config" + preserves the existing
  file-plane config across engine migration.

Lane D.1 (recipe text):
- embeddingMismatchMessage() takes an `engineKind` arg. PGLite branch
  emits a wipe-and-reinit recipe using gbrainPath('brain.pglite') or
  the caller's databasePath override. Postgres branch keeps the SQL
  ALTER recipe.
- The PGLite recipe recommends `gbrain reinit-pglite` (new sugar
  command below) as the one-line path before falling back to the
  by-hand mv + init + sync sequence.

Lane D.4 (sync help dispatch):
- `sync` and `reinit-pglite` added to CLI_ONLY_SELF_HELP so their own
  --help branches reach the user (pre-fix the generic short-circuit
  fired first and the dedicated usage was unreachable; codex round 2
  CDX2-12).
- `gbrain sync --help` short-circuits BEFORE engine bind so users on
  a fresh tmpdir (no config) can read the help without hitting
  no-such-config errors.

Sugar:
- New `gbrain reinit-pglite --embedding-model X --embedding-dimensions N`
  wraps the wipe + init + sync dance into one command. Backs up the
  brain to <path>.bak. TTY confirmation unless --yes. --no-sync to
  defer the resync. --json for scripts.

Tests:
- test/cli.test.ts sync-help test rewritten for the new
  per-command-usage output (lists --no-embed which is the v0.37
  user-visible flag the wave wanted to surface).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(embed+sync): pre-flight dim-mismatch guard + sync hint at both catch sites

embedding-pipeline error UX. Pre-fix, a fresh-install dim mismatch
produced raw Postgres "expected N dimensions, not M" errors page after
page, surfacing only after the worker pool drained the entire corpus.
Sync swallowed embed errors at TWO catch sites and never surfaced
the recovery recipe.

embed.ts:
- New `EmbeddingDimMismatchError` tagged class with the paste-ready
  recipe baked in.
- `runEmbedCore` pre-flights via `readContentChunksEmbeddingDim` +
  gateway.getEmbeddingDimensions() before the worker pool spins up.
  On mismatch, throws the typed error which the CLI wrapper catches
  and prints. Dry-run skips the check (no embed risk).
- Catches the headline fresh-install bug class at first call instead
  of letting it hammer N parallel API calls into dim-rejected inserts.

sync.ts:
- Both embed catches at sync.ts:990 (incremental) and sync.ts:1129
  (first-sync) detect EmbeddingDimMismatchError and surface the recipe
  + a `--no-embed` tip on stderr (codex round 2 CDX2-8: incremental
  path was previously silent; only the first-sync path was flagged).
- Non-mismatch embed failures still stay best-effort (rate limits,
  transient network) — those shouldn't break sync.
- Sync calls runEmbedCore directly instead of runEmbed (which calls
  process.exit on error and bypasses sync's catch).
- Sync gets a proper --help block listing every meaningful flag:
  --no-embed, --workers, --source, --skip-failed, --retry-failed,
  --watch, --interval, --no-pull, --all, --json, --yes, --dry-run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(doctor): read gateway for schema-sizing checks + provider-aware key lookup

Doctor's embedding checks were reading the DB config table for
embedding_model / embedding_dimensions / zeroentropy_api_key. Post
v0.37 the file plane is canonical (the DB plane is schema-applied
metadata, not runtime gateway config) so those reads produced stale
verdicts on fresh installs whose DB row hadn't been written.

- checkEmbeddingWidthConsistency reads gateway.getEmbeddingDimensions()
  and gateway.getEmbeddingModel() instead of engine.getConfig(...).
  Reuses readContentChunksEmbeddingDim from the same shared helper
  init + embed use. On mismatch, the fix hint threads engineKind +
  databasePath into the new branched recipe (codex round 1 CDX-8 +
  Lane E.1/E.2).
- checkZeEmbeddingHealth reads gateway for the model + loadConfigFileOnly
  for the key. Fires when (a) resolved model starts with zeroentropyai:
  AND (b) ZEROENTROPY_API_KEY is unset in env AND (c) file plane has
  no zeroentropy_api_key (codex round 2 CDX2-10).
- loadRecommendationContext reads gateway for both fields and
  recognizes the ZE key alongside OpenAI/Anthropic in the
  hasEmbeddingApiKey check, so brains on ZE no longer look "healthy"
  just because OPENAI_API_KEY happens to be set (codex round 2 CDX2-11).

Tests rewritten for the gateway-source-of-truth contract via
configureGateway() in beforeAll. Added a "gateway unconfigured: skips
with ok" case so doctor doesn't false-warn on cold-boot brains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test+docs(v0.37): fix-wave unit coverage + PGLite-first migration recipe + TODOS

Lands the v0.37 PGLite fresh-install fix wave's structural tests and
the user-facing migration recipe overhaul.

test/v0_37_fix_wave.test.ts (new): 22 unit cases pinning the lanes:
- Lane A: defaults module exports, getPGLiteSchema/getPostgresSchema
  default-args, registry + isCacheSafe under the `cfg > gateway >
  DEFAULT` chain (both gateway-set and gateway-reset branches).
- Lane B: loadConfigFileOnly env isolation + DATABASE_URL inference
  refusal + null-on-missing.
- Lane C.3: buildGatewayConfig maps zeroentropy_api_key + process.env
  wins over config (operator escape hatch contract).
- Lane D.2: EmbeddingDimMismatchError shape + tag.
- Lane D.4: structural assertion that `sync` is in CLI_ONLY_SELF_HELP.
- Deferred-TODO ship: reinit-pglite is registered correctly +
  embeddingMismatchMessage PGLite branch recommends it.

docs/embedding-migrations.md: PGLite section moved to top (the default
install). The recommended path is `gbrain reinit-pglite` one-liner;
the by-hand mv + init + sync sequence stays as the fallback recipe.
Postgres SQL ALTER recipe preserved. New section on `gbrain config
set` refusal explains the file-plane vs DB-plane contract so users
don't follow stale documentation.

TODOS.md: 4 deferred follow-ups filed with concrete file pointers:
- gbrain embed --try-fallback (provider auto-switch with consent gate)
- Full plane unification for non-schema-sizing fields
- Worker-pool shared AbortController for mid-run dim drift
- Cleanup of back-compat constants in src/core/embedding.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(v0.37): fill behavior gaps + headline fresh-install E2E

The structural fix-wave tests in test/v0_37_fix_wave.test.ts pin lane-level
invariants (exports, registry chain, signature shapes). The audit found 10+
END-TO-END behaviors that the structural tests didn't actually reach.
This file fills the highest-leverage gaps.

Unit coverage (test/v0_37_gap_fill.test.ts, 12 cases):
- Lane A.7: chunk-row INSERT default tracks DEFAULT_EMBEDDING_MODEL
  constant (pre-fix this was the literal 'text-embedding-3-large' at
  pglite-engine.ts:1611 + postgres-engine.ts:1647 — production write
  sites that were never directly tested; codex round 2 CDX2-4).
- Lane A.8: schema seed stores full provider:model in DB config
  (pre-fix the .split(':') strip dropped the prefix; codex round 1
  CDX-4). Asserts a fresh ZE init stores `zeroentropyai:zembed-1`
  in the config table, not bare `zembed-1`.
- Lane B precedence: explicit CLI > env > existing file > default
  test (codex round 2 CDX2-7 contradiction guard).
- Lane C.3 env merge: process.env.ZEROENTROPY_API_KEY threads through
  loadConfig → cfg.zeroentropy_api_key; loadConfigFileOnly does NOT.
- Lane D.2 end-to-end: schema=1536 + gateway=1280 →
  EmbeddingDimMismatchError fires AND the embed transport is never
  called (the whole point of pre-flight). Plus dry-run skips the
  check.
- Lane D.3 source-text grep: both sync.ts catch sites detect the
  typed error + the `--no-embed` tip is present (CDX2-8).
- Lane E.4 source-text grep: loadRecommendationContext is
  provider-aware (reads gateway + branches on ZE/OpenAI key).
- reinit-pglite contract: refuses on non-PGLite engines + refuses
  when required flags are missing.

E2E (test/e2e/fresh-install-pglite.test.ts, 2 cases):
- Bare `gbrain init --pglite` produces a `vector(1280)` schema, prints
  the resolved choice, persists defaults to config.json — the headline
  scenario that v0.37 ships to fix.
- init → seed page → embed end-to-end: chunks have non-null
  embeddings; no dim mismatch despite the wave's defaults change.

Both E2E cases are IN-PROCESS (per CDX2-12: CLI-subprocess E2E can't
inherit `__setEmbedTransportForTests`). They run with stubbed transport
returning synthetic 1280-dim vectors so we never hit real provider APIs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(v0.37): defensive gateway restore in reinit-pglite describe block

Adds an afterAll that restores the gateway to OpenAI/1536 (matching the
bunfig preload) at the end of the reinit-pglite describe. Belt-and-
suspenders: earlier describe blocks in this file already restore, but
if the reinit-pglite tests ever start mutating the gateway in the
future, this protects downstream test files in the same bun-test shard
from inheriting a non-default state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.37.10.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: scrub stale config-set recipes for embedding model (v0.37.10.0)

README + topologies + embedding-providers were still pointing users at
`gbrain config set embedding_model X` / `embedding_dimensions N`. As of
v0.37.10.0 those writes are refused — the schema column has to resize
alongside the config. Point at `gbrain reinit-pglite` (PGLite) and the
SQL recipe in `docs/embedding-migrations.md` (Postgres) instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: bump version to v0.37.11.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: quarantine v0.37 fix-wave tests to .serial.test.ts

CI's `check:test-isolation` lint flagged R1 violations (direct
`process.env.GBRAIN_HOME` mutation) in both new fix-wave test files.
Per the documented quarantine pattern in CLAUDE.md, rename to
`*.serial.test.ts` instead of refactoring through `withEnv()` — both
files use beforeEach/afterEach env wiring that's already serial-safe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:11:00 -07:00

16 KiB

GBrain Deployment Topologies

GBrain supports three deployment shapes. They compose: a single user can mix all three on the same machine without conflict, because every shape resolves to "which ~/.gbrain/config.json is active right now?" and GBRAIN_HOME controls that selection.

This page covers the three topologies, when each fits, and concrete setup recipes. Pair this doc with docs/architecture/brains-and-sources.md (which covers the in-brain organization axes) — that doc is about WHICH database; this doc is about WHERE that database lives.

Quick decision tree

   "I'm setting up gbrain..."
        │
        ▼
  Just for me, on one machine? ─── yes ───▶ Topology 1 (single brain)
        │
        no
        │
        ▼
  Will a remote machine host the brain
  while my agent runs locally? ──── yes ───▶ Topology 2 (cross-machine thin client)
        │
        no
        │
        ▼
  Multiple Conductor worktrees that
  shouldn't share a code index? ─── yes ───▶ Topology 3 (split-engine)

Topologies 2 and 3 stack: a thin-client install can also host per-worktree code engines, and a per-worktree code engine can also point its artifact brain at a remote server.

Topology 1 — Single brain (today's default)

  ┌────────────────┐
  │   one machine  │
  │  ┌──────────┐  │
  │  │  gbrain  │──┼──→  ~/.gbrain/  →  PGLite  or  Supabase
  │  │   CLI    │  │
  │  └──────────┘  │
  └────────────────┘

What you get: one local DB (PGLite for small brains, Supabase for ~1000+ files). All commands work directly against it. gbrain serve exposes it to a single agent over MCP.

When it fits: solo use, single machine, one agent, no Conductor parallelism. This is the default; gbrain init (no flags) gives you this.

Setup:

gbrain init           # interactive — defaults to PGLite
gbrain init --pglite  # explicit local
gbrain init --supabase  # remote Supabase (recommended for 1000+ files)

Nothing else here is special. The other two topologies are variations on "who owns the DB" and "how does the agent talk to it."

Topology 2 — Cross-machine thin client

  ┌────────────┐                    ┌──────────────────┐
  │ neuromancer│                    │    brain-host    │
  │ ┌────────┐ │ HTTP MCP / OAuth   │  ┌────────────┐  │
  │ │ Hermes │─┼───────────────────→│  │   gbrain   │──┼──→ Supabase
  │ │ agent  │ │                    │  │ serve --http│  │
  │ └────────┘ │                    │  └────────────┘  │
  │            │                    │   (with autopilot)│
  │  no local  │                    │                  │
  │  gbrain DB │                    │                  │
  └────────────┘                    └──────────────────┘

What you get: the agent on one machine ("neuromancer") consumes a brain hosted on another machine ("brain-host") over HTTP MCP with OAuth. The agent's machine has NO local engine. All queries, searches, embeddings, and indexing happen on the host.

When it fits:

  • Heavy brain (Supabase + autopilot) lives on a beefy machine; agents elsewhere just consume it.
  • You want one source of truth across many machines.
  • Spinning up a parallel local install would create source-ID contention or duplicate work.

The thin client's ~/.gbrain/config.json carries a remote_mcp field instead of a local DB connection:

{
  "engine": "postgres",  // ignored — never used
  "remote_mcp": {
    "issuer_url": "https://brain-host.local:3001",
    "mcp_url":    "https://brain-host.local:3001/mcp",
    "oauth_client_id": "neuromancer-...",
    "oauth_client_secret": "..."  // or set GBRAIN_REMOTE_CLIENT_SECRET
  }
}

The CLI dispatch guard refuses any DB-bound command (sync, embed, extract, migrate, apply-migrations, repair-jsonb, orphans, integrity, serve) on a thin-client install with a clear error pointing at the remote host. gbrain doctor runs a dedicated thin-client check set (OAuth discovery, token round-trip, MCP smoke).

Setup

Step 1 — On the host (brain-host):

gbrain init --supabase                         # or --pglite, doesn't matter
gbrain serve --http --port 3001 --bind 0.0.0.0 # v0.34: bind explicitly for remote access
                                                # (defaults to 127.0.0.1 since v0.34)
gbrain auth register-client neuromancer \
  --grant-types client_credentials \
  --scopes read,write,admin                    # admin needed for ping/doctor

# v0.34: source-scoped client (write to one source, federate reads across
# multiple sources). Omit both flags for a v0.33-compatible super-client.
gbrain auth register-client neuromancer-dept \
  --grant-types client_credentials \
  --scopes read,write \
  --source dept-x \
  --federated-read dept-x,shared,parent-canon

The register-client command prints a client_id and client_secret. Note both. Scope must include adminsubmit_job (used by gbrain remote ping) and run_doctor (used by gbrain remote doctor) both require it.

Step 2 — On the thin client (neuromancer):

gbrain init --mcp-only \
  --issuer-url https://brain-host.local:3001 \
  --mcp-url https://brain-host.local:3001/mcp \
  --oauth-client-id <id> \
  --oauth-client-secret <secret>

Pre-flight smoke runs three probes (OAuth discovery, token round-trip, MCP initialize). If any fails, init exits with an actionable error. On success, ~/.gbrain/config.json gets remote_mcp set and NO local DB is created.

Step 3 — Configure your agent's MCP client.

For Claude Desktop / Hermes / openclaw, add a single MCP server entry pointing at the host's mcp_url with the bearer token from register-client. Example for Claude Desktop's ~/.config/claude/claude_desktop_config.json:

{
  "mcpServers": {
    "gbrain": {
      "type": "url",
      "url": "https://brain-host.local:3001/mcp",
      "headers": { "Authorization": "Bearer <client_secret>" }
    }
  }
}

Step 4 — Verify.

gbrain doctor             # runs thin-client checks (no local DB needed)
gbrain remote ping        # triggers an autopilot cycle on the host (Tier B)
gbrain remote doctor      # asks the host to run its own doctor (Tier B)

gbrain sync and friends will refuse with a clear thin-client error naming the mcp_url. That's the correct behavior — those commands need a local engine that doesn't exist here.

Re-run guard

Running gbrain init (no flags) on a machine that already has thin-client config set refuses without --force. This catches the scripted-setup-loop friction where an orchestrator keeps trying to create a local DB. Use gbrain init --mcp-only --force to refresh thin-client config.

Storing the OAuth secret

Three storage paths in priority order:

  1. GBRAIN_REMOTE_CLIENT_SECRET env var (preferred for headless agents). When set, overrides whatever's in the config file. The init flow doesn't persist a config-file copy when the env var was the source.
  2. ~/.gbrain/config.json with 0600 perms (default for interactive setup; mirrors how Supabase keys are stored today).
  3. macOS Keychain integration is on the roadmap; not in v1.

Topology 3 — Split-engine, per-worktree code + remote artifacts

  ┌──────────────────────────────────────────────────────┐
  │                  one machine                         │
  │                                                      │
  │  ┌─ worktree A ──────────────┐                       │
  │  │  GBRAIN_HOME=A/.conductor │                       │
  │  │  gbrain serve --port 3001 │── PGLite (code A)     │
  │  └───────────────────────────┘                       │
  │                                                      │
  │  ┌─ worktree B ──────────────┐                       │
  │  │  GBRAIN_HOME=B/.conductor │                       │
  │  │  gbrain serve --port 3002 │── PGLite (code B)     │
  │  └───────────────────────────┘                       │
  │                                                      │
  │  ┌─ default ~/.gbrain ───────┐    HTTP MCP / OAuth   │
  │  │  gbrain serve --port 3000 │──────────────────────→ remote artifacts
  │  └───────────────────────────┘                        (Supabase / brain-host)
  │                                                      │
  │  Agent's MCP config (Hermes / Claude Desktop):       │
  │    mcp__gbrain_code__*       → http://localhost:3001 │
  │    mcp__gbrain_artifacts__*  → http://brain-host/mcp │
  └──────────────────────────────────────────────────────┘

What you get: each Conductor worktree has its own per-worktree code index (local PGLite, disposable when the worktree dies). Artifacts (plans, learnings, transcripts) still live in a shared brain that all worktrees can see and write to.

When it fits:

  • Multiple Conductor worktrees on one machine, all touching the same code repo.
  • You don't want each worktree's code-import to clobber the others' last_commit, source IDs, or symbol tables.
  • You DO want artifacts (plans, learnings, retros, transcripts) to be visible across worktrees.

How it works

GBRAIN_HOME selects which ~/.gbrain directory is active. Set per worktree:

export GBRAIN_HOME=/path/to/worktree-A/.conductor/gbrain
gbrain init --pglite
gbrain serve --http --port 3001

Each worktree's gbrain serve instance binds its own port and indexes its own DB. Multiple gbrain serve processes coexist fine — they're separate OS processes with separate config and separate connection pools.

The artifact brain runs as a separate gbrain serve instance with the default ~/.gbrain (no GBRAIN_HOME override) — or remote, in which case it's a Topology 2 setup.

The agent's MCP client config lists multiple servers, each with a unique alias. Tool names are namespaced as mcp__<alias>__<tool>, so the agent calls mcp__gbrain_code__search for code lookups and mcp__gbrain_artifacts__search for artifact lookups.

Per-worktree code brains index source files only — no meeting notes, no people pages, no transcripts. Configure each code brain to use Voyage's code-tuned model at init time so the config can't be lost to a later init overwrite:

export GBRAIN_HOME=/path/to/worktree-A/.conductor/gbrain
gbrain init --pglite \
  --embedding-model voyage:voyage-code-3 \
  --embedding-dimensions 1024

voyage-code-3 is Voyage's code-specialized embedding model with head-to-head numbers above their general flagships on code retrieval (voyageai.com/blog). For already-initialized brains, switch with the one-command wipe-and-reinit (preserves every other config field):

gbrain reinit-pglite --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024
gbrain reindex --code --yes

(gbrain config set embedding_model is refused as of v0.37.11.0 because the schema column has to resize alongside the config.)

gbrain reindex --code prints a recommendation when the configured embedding model isn't code-tuned. Suppress with GBRAIN_NO_CODE_MODEL_NUDGE=1 if you've intentionally chosen another provider (single-vendor procurement, compliance, no Voyage key).

CRITICAL: alias-level routing is manual

Topology 3 has no smart per-tool routing inside gbrain. The agent picks which brain to query when it picks the alias. A wrong alias writes (or queries) the wrong brain silently. This is intentional (explicit beats magic) but real:

  • If the agent calls mcp__gbrain_artifacts__put_page with code-shaped content, that page lands in the artifact brain forever.
  • If the agent calls mcp__gbrain_code__search for a question that actually wants artifact context, the search comes back empty.

Mitigations:

  • Name aliases clearly. gbrain_code vs gbrain_artifacts is unambiguous; gbrain vs gbrain_local is not.
  • Document in your agent's system prompt or rules which alias goes where. Be explicit about "code questions → gbrain_code; everything else → gbrain_artifacts."
  • Pair Topology 3 with gstack's per-worktree wiring (which sets the alias names + agent rules consistently across worktrees).

Setup (manual; gstack automates this side)

The gbrain side requires zero new code — GBRAIN_HOME and --port already exist. Setup looks like:

# Start the artifact brain (default ~/.gbrain) on port 3000
gbrain serve --http --port 3000 &

# Start a per-worktree code brain on port 3001
export GBRAIN_HOME=/path/to/worktree-A/.conductor/gbrain
gbrain init --pglite
gbrain serve --http --port 3001 &
unset GBRAIN_HOME

Then configure the agent's MCP config with two entries (different aliases, different ports). For Claude Desktop:

{
  "mcpServers": {
    "gbrain_artifacts": {
      "type": "url",
      "url": "http://localhost:3000/mcp",
      "headers": { "Authorization": "Bearer <token-A>" }
    },
    "gbrain_code": {
      "type": "url",
      "url": "http://localhost:3001/mcp",
      "headers": { "Authorization": "Bearer <token-B>" }
    }
  }
}

The gstack-side wiring (per-worktree home setup, port allocation, automatic MCP config generation, gitignore for the per-worktree DB) is in the gstack repo's setup-gbrain skill — it composes these primitives, gbrain doesn't have to know about Conductor.

Combining topologies

The three shapes compose. A single machine can run:

  • A thin-client default config pointing at a remote artifact brain (Topology 2).
  • Plus per-worktree code brains under their own GBRAIN_HOME (Topology 3).
  • Each worktree's gbrain serve instance is local; the agent's MCP config lists them alongside the remote artifact brain.

GBRAIN_HOME controls which config file is active for any one CLI invocation. gbrain serve --port controls which port a server listens on. The agent's MCP client picks the alias and thus the destination per tool call. There's no global gbrain orchestrator that knows about all of them simultaneously — that's by design.

When NOT to use these topologies

  • Don't use Topology 2 if your agent only ever runs on the same machine as the brain. A local gbrain install + gbrain serve (stdio) is simpler and faster.
  • Don't use Topology 3 if you only have one Conductor worktree at a time. Per-worktree engines exist to prevent contention; one-at-a-time use has no contention.
  • Don't use a remote_mcp thin client AND a local engine on the same machine in the same GBRAIN_HOME. The dispatch guard refuses DB-bound commands when remote_mcp is set. If you genuinely want both modes on one machine, use GBRAIN_HOME to separate them (one home for the thin client, another for the local engine).

See also

  • docs/architecture/brains-and-sources.md — in-brain organization (brains vs sources axes).
  • docs/mcp/CLAUDE_DESKTOP.md and siblings — per-client MCP setup.
  • gbrain init --help and gbrain auth --help for command-level details.