* fix(sync): infiniteGameExp + foxhoundinc 5-bug wave (#1422, #1433, #1434, #1309, #1436) Five real production bugs from infiniteGameExp (PostgreSQL onboarding) and foxhoundinc (dream-cycle reproduction), each silent-failure shape where gbrain told the user the operation succeeded when it didn't. * #1422 — `gbrain dream` swallowed connectEngine errors. Bind the caught error and surface `[dream] WARNING: could not connect to DB (...)` on stderr before falling through to filesystem-only phases. runDream(null) no-DB fallback preserved. * #1433 — `gbrain sync` deleted previously-indexed log.md / schema.md / index.md / README.md pages on every re-sync. Refactor isSyncable through private classifySync helper; expose unsyncableReason (companion returning the same tagged reason) and SYNC_SKIP_FILES named export. Cleanup loop guards on reason === 'metafile' before deleting. * #1434 — `gbrain sync` without --source on single-vault brains routed to source_id='default' (zero pages) and silently failed. Add resolver tier 5.5 'sole_non_default' AFTER brain_default (explicit user intent wins). Wire runSync + runImport to call resolveSourceWithTier unconditionally so the tier actually fires. Stderr nudge on tier hit; suppress with GBRAIN_NO_SOLE_NON_DEFAULT_NUDGE=1. * #1309 — overlapping ingest roots created duplicate pages. New BrainEngine.findDuplicatePage?(sourceId, {hash, frontmatterId}) with identity-based posture: SKIP when frontmatter.id matches (true external duplicate), WARN-ALWAYS on content_hash collision with different/missing fm.id, FAIL CLOSED on lookup error. Migration v95 adds partial index pages_dedup_idx (Postgres CONCURRENTLY, PGLite plain CREATE). * #1436 — MCP fuzzy get_page returned slug candidates from sources outside caller's scope. resolveSlugs signature extended with {sourceId?, sourceIds?} matching the sourceScopeOpts helper output; operations.ts threads it through. Both engines preserve unscoped back-compat for internal CLI callers. Plus a stable tiebreaker on searchVector ORDER BY (score DESC, page_id ASC, chunk_id ASC) in both engines. Caught while wiring the index above — basis-vector eval fixtures with tied scores depend on planner row order, which any new index on pages could flip. Pins eval-replay-gate ranking determinism against future index changes. Per codex review of the original plan: caught 6 load-bearing gaps that the engineering review missed (runSync bypass, #1436 misclassified as fixed, dedup fail-open, content-hash-alone too aggressive, soft-delete filter missing, tier-ordering contradiction). All folded in pre-merge. Tests: 65 new wave cases across 7 new files + 1 extended; all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.41.13.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.0 KiB
Brain Routing Convention
Cross-cutting rules for which brain and which source an operation targets.
Applies to every skill that reads or writes brain pages. Full mental model
lives in docs/architecture/brains-and-sources.md — read it once.
The two axes (one-line summary)
- Brain = which DATABASE.
--brain,GBRAIN_BRAIN_ID,.gbrain-mount. - Source = which REPO INSIDE the database.
--source,GBRAIN_SOURCE,.gbrain-source.
Orthogonal. Pick one on each axis per operation.
Default behavior (ALWAYS)
Start in the brain + source resolved by the environment:
- Run
gbrain mounts listif you haven't seen the user's mounts yet. - Trust the resolver. If the user is in
~/team-brains/media/, their.gbrain-mountpins brain=media-team. Don't override that silently. - For every brain op, pass the resolved brain id explicitly when calling tools (even if it matches the default). Makes routing visible in logs.
Bare gbrain query "X" routes to the default brain's default source. That
is the right answer 90% of the time. Don't cross the boundary without a
reason.
When to switch brain
Switch brain (--brain <id>) when:
- The user's question is specifically about a team the user belongs to ("what did team X decide?", "what's the status of project Y at team X?"). Switch BEFORE searching, not after a failed search in host.
- The user is asking you to ingest data that belongs to a specific team (meeting notes from a team meeting, letters from a team's pipeline). The data owner determines the brain.
- The user explicitly names a team/brain ("check the media-team brain for...").
Do NOT switch brain when:
- The user asks a general question that might pull from anywhere. Start in host, then cross-query on-demand if host doesn't have it.
- You're unsure. Stay in host, surface what you found, let the user point you at a specific brain.
Source resolution chain (7-tier, v0.41.13+)
gbrain resolves the active source via resolveSourceId() in
src/core/source-resolver.ts. Seven tiers, highest priority first:
| # | Tier | Signal |
|---|---|---|
| 1 | flag |
Explicit --source <id> CLI flag (or --source-id <id> on gbrain extract / gbrain import) |
| 2 | env |
GBRAIN_SOURCE environment variable |
| 3 | dotfile |
.gbrain-source file in CWD or any ancestor directory |
| 4 | local_path |
A registered source whose local_path contains CWD (longest prefix wins) |
| 5 | brain_default |
Brain-level sources.default config key (explicit user intent) |
| 5.5 | sole_non_default |
When tiers 1–5 missed AND exactly one registered source has a local_path AND isn't 'default', auto-route to it. Fires a one-time stderr nudge per CLI invocation. Suppress with GBRAIN_NO_SOLE_NON_DEFAULT_NUDGE=1. |
| 6 | seed_default |
Literal 'default' (always exists post-migration v16) |
v0.41.13 tier 5.5 (sole_non_default): added for single-source brains
(typical for users with one Obsidian vault, one notes folder, one project).
Pre-fix, gbrain sync from /tmp against a brain registering only
studiovault silently routed to 'default' and every edit failed at
createVersion because the slug didn't exist there. The tier auto-routes
to the obvious single answer. Multi-source brains (2+ non-default registered)
still fall through to seed_default and require explicit --source.
Placement AFTER brain_default is deliberate: a user who explicitly set
sources.default via gbrain sources default <id> has stated intent that
wins over the auto-route. Archived sources are excluded from the count.
v0.37.7.0 tooling:
gbrain sources current [--json]echoes the resolved source AND which tier won. Run this before any destructive op to verify what you're about to target.gbrain sources current --source Xshows what an explicit flag WOULD resolve to (validates X exists in the sources table).
CLI commands honoring this chain: gbrain sync, gbrain import,
gbrain search, gbrain extract (via --source-id <id> since
--source is the fs|db data-source axis), gbrain graph-query
(via --include-foreign for cross-source traversal).
Trust boundary (v0.34.1.0): the resolver is CLI-layer only.
Operations.ts handlers do NOT read .gbrain-source or
GBRAIN_SOURCE. MCP/remote callers go through
ctx.auth.sourceId / ctx.auth.allowedSources instead. A remote
caller cannot inherit the server process's CLI source context.
When to switch source
Switch source (--source <id>) when:
- The user is working in a specific repo (the
.gbrain-sourcedotfile usually handles this — don't fight it). - The user asks about something scoped to a repo ("what's in my gstack notes about retry policy?").
- You're writing a page that logically belongs to one repo. The data origin determines the source.
Do NOT switch source when:
- The user's intent crosses repos. Keep
federated=truesources for cross-source search. - You'd lose a cross-repo match by isolating.
Cross-brain queries (latent-space federation)
v0.19 does NOT do deterministic cross-brain federation. No SQL fan-out. No unified ranking. The AGENT federates.
Pattern when the user asks something that might span brains:
- Query host with the obvious query.
- Check
gbrain mounts listfor relevant brain ids. - If you think another brain has the answer, re-query THAT brain
explicitly (
--brain <id>). - Synthesize across results. Cite
<brain>:<source>:<slug>so the user can trace.
Never silently mix brains. Every finding is citable to its brain.
Writing across brains
Writing is stricter than reading. ASK before writing cross-brain.
- A fact about a team's work → team's brain, not host.
- A fact the user confirmed about a person ONLY they know → host/personal, not a team brain.
- An enrichment discovered from public data → usually host unless the user says otherwise.
If you're about to put_page --brain <team-brain>, confirm with the user
unless they explicitly said "save this to team-X". Default brain for
writes is the user's personal brain.
Citations with brain context
Standard citation format stays the same ([Source: ...]), but when pages
come from a mounted brain, add the brain context for human traceability:
- Single-brain query:
[Source: Meeting, 2026-04-10](unchanged). - Cross-brain synthesis:
[Source: media-team:meetings/2026-04-10]or[Source: policy-team:research/retry-budgets].
This matches v0.18.0's source-aware citation ([source-id:slug]) extended
with a brain prefix when relevant.
Decision table
| Situation | Brain | Source |
|---|---|---|
| User cd's into a team-brain checkout and asks a general question | dotfile-resolved team brain | dotfile-resolved source |
| User asks "what did team X decide?" | team-x explicitly |
resolver default |
| User asks "what are we doing across all teams?" | fan out across mounts, agent-driven | resolver default |
| User asks "add this to my gstack notes" | host | gstack |
| User asks "save this meeting note for team X" | team-x (confirm if ambiguous) |
team's meetings source |
| User asks "write me an essay" | host (personal) | essays |
| Unknown — can't classify | stay in host, ask the user | resolver default |
Anti-patterns
- Silently jumping brains to "find" an answer when the user clearly meant host. That's an audit-trail hole.
- Writing to host when the data is clearly team-owned ("the team's plans are now in your personal brain" = bad surprise).
- Cross-brain federation in a single query without citations that name the source brain. The user cannot trace the answer back.
- Ignoring
.gbrain-mount/.gbrain-sourcedotfiles. They're load-bearing context — the user set them up for a reason.
Read more
docs/architecture/brains-and-sources.md— the full mental model with topology diagrams (single-person, personal-with-repos, CEO-class with multiple team brains).skills/conventions/brain-first.md— reads the brain BEFORE asking.skills/conventions/quality.md— citation format (extended here with brain prefix).