Files
gbrain/docs/architecture/thin-client.md
T
Garry TanandClaude Opus 4.8 c825ef8f7b refactor(docs): relocate Key files / thin-client / Testing out of CLAUDE.md (verbatim)
CLAUDE.md had grown to 592KB / ~147k tokens auto-loaded every session (~77% of
the llms-full.txt single-fetch bundle). The per-file index was append-only by
mandate. This is the exact thin-dispatcher-vs-fat-blob anti-pattern gbrain exists
to fix, so CLAUDE.md becomes a thin orientation + resolver that points at
on-demand docs.

This commit is the VERBATIM move (content-preserving — the next commit compresses):
- docs/architecture/KEY_FILES.md   <- ## Key files + the calibration key-files
  cluster + Schema Cathedral v3 impl detail
- docs/architecture/thin-client.md <- ## Thin-client routing
- docs/TESTING.md                   <- ## Testing
- ## Commands DROPPED (18 'added in vX.Y' history blocks; current surface is
  gbrain 0.41.38.0 -- personal knowledge brain

USAGE
  gbrain <command> [options]

SETUP
  init [--pglite|--supabase|--url]   Create brain (PGLite default, no server)
  migrate --to <supabase|pglite>     Transfer brain between engines
  upgrade                            Self-update
  check-update [--json]              Check for new versions
  doctor [--json] [--fast]            Health check (resolver, skills, pgvector, RLS, embeddings)
  integrations [subcommand]          Manage integration recipes (senses + reflexes)

PAGES
  get <slug>                         Read a page
  put <slug> [< file.md]             Write/update a page
  delete <slug>                      Delete a page
  list [--type T] [--tag T] [-n N]   List pages

SEARCH
  search <query>                     Keyword search (tsvector)
  query <question> [--no-expand]     Hybrid search (RRF + expansion)
  ask <question> [--no-expand]       Alias for query

IMPORT/EXPORT
  import <dir> [--no-embed]          Import markdown directory
  sync [--repo <path>] [flags]       Git-to-brain incremental sync
  sync --watch [--interval N]        Continuous sync (loops until stopped)
  sync --install-cron                Install persistent sync daemon
  export [--dir ./out/]              Export to markdown
  export --restore-only [--repo <p>] Restore missing supabase-only files
        [--type T] [--slug-prefix S] With optional filters

FILES
  files list [slug]                  List stored files
  files upload <file> --page <slug>  Upload file to storage
  files upload-raw <file> --page <s> Smart upload (size routing + .redirect.yaml)
  files signed-url <path>            Generate signed URL (1-hour)
  files sync <dir>                   Bulk upload directory
  files verify                       Verify all uploads

EMBEDDINGS
  embed [<slug>|--all|--stale]       Generate/refresh embeddings

LINKS
  link <from> <to> [--type T]        Create typed link
  unlink <from> <to>                 Remove link
  backlinks <slug>                   Incoming links
  graph <slug> [--depth N]           Traverse link graph (returns nodes)
  graph-query <slug> [--type T]      Edge-based traversal with type/direction filters
        [--depth N] [--direction in|out|both]

TAGS
  tags <slug>                        List tags
  tag <slug> <tag>                   Add tag
  untag <slug> <tag>                 Remove tag

TIMELINE
  timeline [<slug>]                  View timeline
  timeline-add <slug> <date> <text>  Add timeline entry

TOOLS
  extract <links|timeline|all>       Extract links/timeline (idempotent)
        [--source fs|db]             fs (default) walks .md files; db iterates engine pages
        [--dir <brain>]              brain dir for fs source
        [--type T] [--since DATE]    filters (db source)
        [--dry-run] [--json]
  publish <page.md> [--password]     Shareable HTML (strips private data, optional AES-256)
  check-backlinks <check|fix> [dir]  Find/fix missing back-links across brain
  lint <dir|file> [--fix]            Catch LLM artifacts, placeholder dates, bad frontmatter
  orphans [--json] [--count]         Find pages with no inbound wikilinks
  salience [--days N] [--kind P]     v0.29: pages ranked by emotional + activity salience
  anomalies [--since D] [--sigma N]  v0.29: cohort-based statistical anomalies (tag, type)
  transcripts recent [--days N]      v0.29: recent raw .txt transcripts (local-only)
  dream [--dry-run] [--json]         Run the overnight maintenance cycle once (cron-friendly).
                                     See also: autopilot --install (continuous daemon).
  check-resolvable [--json] [--fix]  Validate skill tree (reachability/MECE/DRY)
  report --type <name> --content ... Save timestamped report to brain/reports/

BRAIN (capture / ideate / explore — v0.37/v0.38)
  capture [content] [--file PATH]    Single entrypoint for getting content into the brain
        [--stdin] [--slug s] [--type t]   Inline content / file / stdin; writes to inbox/ by default
        [--source ID] [--quiet|--json]    Multi-source brains: route to a non-default source
  brainstorm <question> [--json]     Bisociation idea generator (hybrid search + far-set + judge)
        [--save|--no-save] [--limit N]
  lsd <question> [--json]            Lateral Synaptic Drift: inverted-judge brainstorm
        [--save|--no-save] [--limit N]    rewarding far-from-obvious + axiomatic inversions

SOURCES (multi-repo / multi-brain)
  sources list                       Show registered sources
  sources add <id> --path <p>        Register a source (id = short name, e.g. 'wiki')
  sources remove <id>                Remove a source + its pages
  sync --all                         Sync all sources with a local_path
  sync --source <id>                 Sync one specific source
  repos ...                          DEPRECATED alias for 'sources' (v0.19.0)

CODE INDEXING (v0.19.0 / v0.20.0 Cathedral II)
  code-def <symbol> [--lang l]       Find the definition of a symbol across code pages
  code-refs <symbol> [--lang l]      Find all references to a symbol (JSON-first)
  code-callers <symbol>              Who calls this symbol? (v0.20.0 A1)
  code-callees <symbol>              What does this symbol call? (v0.20.0 A1)
  query <q> --lang <l>               Filter hybrid search to one language (v0.20.0)
  query <q> --symbol-kind <k>        Filter to symbol type (function|class|method|...) (v0.20.0)
  reconcile-links [--dry-run]        Batch-recompute doc↔impl edges (v0.20.0)
  reindex-code [--source id] [--yes] Explicit code-page reindex (v0.20.0)
  sync --strategy code               Sync code files into the brain

JOBS (Minions)
  jobs submit <name> [--params JSON]  Submit background job [--follow] [--dry-run]
  jobs list [--status S] [--limit N]  List jobs
  jobs get <id>                       Job details + history
  jobs cancel <id>                    Cancel job
  jobs retry <id>                     Re-queue failed/dead job
  jobs prune [--older-than 30d]       Clean old jobs
  jobs stats                          Job health dashboard
  jobs work [--queue Q]               Start worker daemon (Postgres only)

ADMIN
  stats                              Brain statistics
  health                             Brain health dashboard
  history <slug>                     Page version history
  revert <slug> <version-id>         Revert to version
  features [--json] [--auto-fix]     Scan usage + recommend unused features
  autopilot [--repo] [--interval N]  Self-maintaining brain daemon
  config [show|get|set] <key> [val]  Brain config
  storage status [--repo <path>]     Storage tier status and health
        [--json]                     (git-tracked vs supabase-only)
  serve                              MCP server (stdio)
  serve --http [--port N]            HTTP MCP server with OAuth 2.1
    --token-ttl N                    Access token TTL in seconds (default: 3600)
    --enable-dcr                     Enable Dynamic Client Registration
    --public-url URL                 Public issuer URL (required behind proxy/tunnel)
  call <tool> '<json>'               Raw tool invocation
  version                            Version info
  --tools-json                       Tool discovery (JSON)

Run gbrain <command> --help for command-specific help. + the per-command KEY_FILES entries; content stays in git)

CLAUDE.md gains: a Reference map (resolver), a Maintaining section (the
anti-disease rule), and a Cross-cutting invariants subsection under Architecture
so the must-never-violate rules (trust fail-closed, sourceScopeOpts isolation,
JSONB trap, engine parity, contract-first, migrations, multi-source) still
auto-load after the index moved out.

Result: CLAUDE.md 592KB -> 61KB; llms-full.txt 740KB -> 210KB (new docs link-only
until compressed). build-llms drift + budget test green; verify 29/29 green.
The pre-move content is recoverable at git show <this^>:CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:36:50 -07:00

10 KiB

Thin-client routing (remote MCP)

On-demand reference (see CLAUDE.md Reference map). Current behavior + invariants only; release history lives in CHANGELOG.md + git.

gbrain init --mcp-only (v0.29.2) sets up a thin-client install: no local brain content, just an OAuth client pointing at a remote gbrain serve --http. v0.29.2/v0.30.0 only refused 9 obvious local-only commands; the other ~25 silently fell through to connectEngine() and opened the empty local PGLite, returning "No results." against a populated remote brain. v0.31.1 fixes the silent-empty-results bug class for every operation surface.

Key files:

  • src/cli.ts — Routing seam INSIDE the existing op-dispatch path (CDX-1: no parallel src/core/thin-client/ module; routing is a ~80-line conditional in runThinClientRouted). Detects isThinClient(cfg) BEFORE connectEngine so thin-client installs never open the empty PGLite. localOnly ops on thin-client refuse via refuseThinClient (with pinpoint hint table THIN_CLIENT_REFUSE_HINTS). Banner via printIdentityBannerBestEffort before each routed call (suppressed by --quiet, GBRAIN_NO_BANNER=1, non-TTY default). Exhaustive TS never switch on RemoteMcpError.reason for canned, actionable error messages. ENG-2 renderer parity: local-engine path runs JSON.parse(JSON.stringify(result)) so renderers see the same shape on both paths (kills Date/bigint/Buffer drift class).
  • src/core/mcp-client.tscallRemoteTool(config, toolName, args, opts). Hardened in v0.31.1 (CDX-4): all transport errors normalized to RemoteMcpError via the toRemoteMcpError funnel. New CallRemoteToolOptions {timeoutMs, signal}; buildAbortController composes external signal with timeout. New RemoteMcpErrorReason stable union, RemoteMcpErrorDetail.kind ('timeout' | 'aborted' | 'unreachable') sub-tag, RemoteMcpErrorDetail.code field carrying server-supplied error codes (e.g. missing_scope). extractToolErrorCode parses JSON envelopes first, falls back to substring detection for legacy server messages. unpackToolResult<T>(res) unchanged (parses tool-call JSON content). _clearMcpClientTokenCache() test escape.
  • src/core/cli-options.tsparseGlobalFlags adds --timeout=Ns (accepts 30s, 2m, 500ms, plain ms). Default null = per-command default (30s for most ops, 180s for think). parseTimeout(s) exported helper.
  • src/core/doctor-remote.tsgbrain remote doctor adds the oauth_client_scopes_probe check (CDX-5). Probes the read tier via get_brain_identity and admin tier via get_health; reports per-tier status with pinpoint remediation when admin is missing. buildScopeCheck
    • ScopeProbeResult exported for test access. Skippable via GBRAIN_DOCTOR_SKIP_SCOPE_PROBE=1 for fixtures that mock /mcp at JSON-RPC initialize level only (MCP SDK Client hangs on shape mismatch).
  • src/core/ssrf-validate.ts (v0.36 Commit 0) — DNS-rebinding-defended URL validation. validateAndResolveUrl(url) resolves the hostname via dns.lookup({all: true, family: 0}), checks EVERY A AND AAAA record against the internal-IP deny list, returns the resolved IP so callers fetch by IP (defeats DNS rebinding: validation IP === fetch IP). fetchWithSSRFGuard(url, opts) does redirect-aware fetching with per-hop re-validation, max 3 hops by default. Reusable across all URL-fetching features. Test seam __setDnsLookupForTests for hermetic tests.
  • src/core/search/query-intent.ts extension (v0.36 cross-modal wave) — new suggestedModality: 'text' | 'image' | 'both' axis on QuerySuggestions. Module-scope CROSS_MODAL_PATTERNS regex array (compiles once at module load). isAmbiguousModalityQuery(query) heuristic gate fires when a visual noun + reference marker combination indicates genuinely ambiguous routing — used by the Commit 4 LLM tie-break to bound LLM calls to <1% of queries.
  • src/core/search/mode.ts extension (v0.36 cross-modal wave) — ModeBundle extended with 7 cross-modal knobs: cross_modal_both_text_weight / cross_modal_both_image_weight (D6 weighted RRF for 'both' mode, defaults 0.6/0.4), image_query_text_refinement_weight / image_query_image_refinement_weight (D13 hybrid intersect for searchByImage query refinement, defaults 0.4/0.6), unified_multimodal + unified_multimodal_only (Phase 3 unified column routing flags), cross_modal_llm_intent (Commit 4 opt-in escalation). SEARCH_MODE_CONFIG_KEYS extended with 7 corresponding config keys. KNOBS_HASH_VERSION bumped 2→3 (D2 — closes the silent cache-hit class where a cached text-mode result could leak to an image-mode caller).
  • src/core/search/hybrid.ts extension (v0.36 cross-modal wave) — cross-modal routing branch at the embed step. Resolves effectiveModality from per-call opts.crossModal (normalized: literal 'auto' → undefined per D22-1) → suggestions.suggestedModality'text' default. Image route: embedQueryMultimodal + searchVector({embeddingColumn: 'embedding_image'}), skip expansion + keyword (D9 mode-bundle override). 'both' route: parallel text + image vector searches merged via rrfFusionWeighted with effectiveRrfK(baseRrfK, weight) from the configured cross-modal weights. Phase 3 unified routing fires when cfg.search.unified_multimodal === true — bypasses dual-column branching, runs embedQueryMultimodal + searchVector({embeddingColumn: 'embedding_multimodal'}), D8 fail-open on zero rows + not strict-mode falls through to dual-column. Commit 4 LLM escalation fires only when (no explicit per-call opt) AND (regex returned 'text') AND (cfg.search.cross_modal.llm_intent is true) AND (isAmbiguousModalityQuery returns true). Fail-open on every error.
  • src/core/search/image-loader.ts (v0.36 Phase 2) — loadImageInput(input, opts) accepts local path, data: URI, or http(s):// URL. Magic-byte sniff for PNG/JPEG/WebP. Hard size cap (default 10 MB, configurable via search.image_query.max_bytes). For URLs: routes through fetchWithSSRFGuard so DNS rebinding + redirect chains are defeated. Pre-flight Content-Length check + post-fetch size guard for lying servers. ImageLoadError with discriminated code (INVALID_FORMAT / OVERSIZED / INVALID_URL / FETCH_FAILED / TIMEOUT / SSRF_BLOCKED / NOT_FOUND).
  • src/core/search/by-image.ts (v0.36 Phase 2) — searchByImage(engine, input, opts). Always runs image branch (embedQueryMultimodalImage + searchVector(embedding_image)). D13 hybrid intersect: when caller provides optional query, runs parallel text branch via embedQueryMultimodal(query) and merges via rrfFusionWeighted with weights from resolved mode. Phase 3 widens to unified column once search.unified_multimodal=true (transparently upgrades the retrieval quality post-reindex).
  • src/core/spend-log.ts (v0.36 Phase 2 D23-#6) — per-OAuth-client paid-API spend tracking against the mcp_spend_log table (migration v74). checkBudget(engine, clientId, capCents) is the pre-flight gate; throws BudgetExceededError when today's spend has hit the cap. recordSpend(engine, entry) is best-effort post-call. UTC day-aligned aggregation so caps roll over deterministically regardless of server timezone. Local CLI callers (no clientId) bypass the gate. Pre-v0.36 brains without the table fail open to spend=0. VOYAGE_MULTIMODAL_3_PER_IMAGE_CENTS = 0.12 cents per image embed.
  • src/core/search/llm-intent.ts (v0.36 Commit 4) — opt-in LLM tie-break. classifyModalityWithLLM(query, fallback) routes through gateway.chat() with a fixed single-word-output system prompt. 1s timeout via AbortController. parseModality(raw, fallback) is the pure parser — tolerates trailing punctuation + casing. Fail-open on every error (gateway unavailable, timeout, parse failure, unrecognized output) — returns fallback so a misbehaving LLM can never break search. Cost-bounded by the ambiguity heuristic in query-intent.ts (fires <1% of queries when on).
  • src/commands/reindex-multimodal.ts (v0.36 Phase 3) — gbrain reindex --multimodal [--limit N] [--dry-run] [--cost-estimate] [--no-embed] [--yes] [--json]. Walks content_chunks WHERE embedding_multimodal IS NULL, batches via embedMultimodalSafe (Commit 0 partial-failure-aware), persists. D7 lock acquisition via tryAcquireDbLock('gbrain-reindex-multimodal', 360min). Cost prompt + 10s Ctrl-C grace window in TTY. GBRAIN_NO_REEMBED=1 bypass. Checkpoint at ~/.gbrain/reindex-multimodal-checkpoint.json for resume. D23-#2 auto-flip prompt at coverage=100% completion (TTY: interactive; non-TTY: stderr hint with paste-ready command).
  • src/core/backfill-registry.ts extension (v0.36) — new modality backfill kind. SQL filter requires chunk_source='image_asset' AND embedding_image IS NOT NULL AND (modality IS NULL OR modality != 'image'). D22-7 defensive guard: never flag a non-image chunk that happens to have embedding_image populated. Idempotent — second run finds zero rows.
  • src/core/migrate.ts v74 (mcp_spend_log) + v75 (embedding_multimodal_column) — Phase 2 spend-log table + Phase 3 unified column ALTER. v75 is column-only (no HNSW index — deferred to post-reindex per pgvector best practice). v74 uses BTREE on (client_id, created_at) + (token_name, created_at)date_trunc('day', TIMESTAMPTZ) is NOT IMMUTABLE so can't appear in index expressions; range scan on created_at covers the per-day rollup query.
  • src/core/operations.tsget_brain_identity op (read scope, no params, banner-only): cheap counter packet {version, engine, page_count, chunk_count, last_sync_iso} for the thin-client identity banner. Reuses engine.getStats(); banner's 60s client-side TTL bounds frequency to ≤1/60s per CLI process (well below the Fly.io health-check cadence that motivated the original getStats cost warning).
  • src/commands/{salience,anomalies,graph-query,think}.ts — Per-command thin-client routing branches. These commands bypass the operation-layer dispatch in cli.ts (call engine.foo() directly), so each gets its own if (isThinClient(cfg)) { callRemoteTool(...) } branch that maps CLI flags to op params. think is a special case: the server's think op intentionally disables --save/--take for remote callers (operations.ts:1103-1135 trust-boundary gate); thin-client think warns loudly when those flags are set.