Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,140 @@

All notable changes to GBrain will be documented in this file.

## [0.31.1] - 2026-05-08

**Thin-client mode actually works now. `gbrain init --mcp-only` is no longer a half-built bridge.**
**Every read + write + admin op routes through MCP; refused commands carry pinpoint hints.**

Hermes/Neuromancer hit this in production: thin-client install of wintermute (102k pages,
265k chunks). Every CLI search returned zero rows. Exit code 0. No warning. The agent
configured `gbrain init --mcp-only`, then walked into a wall of "no results found" against
a brain that had everything it needed. The CLI was opening the empty local PGLite,
running 38 migrations, and reporting "No results." while the real brain sat untouched
behind an HTTPS endpoint.

v0.31.1 fixes the silent-empty-results bug class for every operation surface gbrain has.
The CLI dispatch layer now routes every non-localOnly op through `callRemoteTool` when
`isThinClient(cfg)` is true, before opening any local engine. Read ops, write ops, admin
ops — all routed. Local-only commands (sync, embed, dream, integrity, etc) refuse with
pinpoint hints naming the closest path: "sync runs on the host. To trigger a remote
cycle, run `gbrain remote ping` (queues an autopilot-cycle job)."

### The numbers that matter

Reproducible against any host with a populated brain. Spin `gbrain serve --http`,
register an OAuth client with `read,write,admin`, then from a second `GBRAIN_HOME`:

| Behavior | v0.30.0 (broken) | v0.31.1 (fixed) |
|---|---|---|
| `gbrain search "X"` against 102k-page host | "No results." (exit 0) | Real rows + identity banner |
| `gbrain stats` on thin-client | Pages: 0, Chunks: 0 | Real numbers from host |
| `gbrain salience` | "(no pages touched in the salience window)" | Real salience output |
| `gbrain put 'wiki/test/foo' --content '...'` | Hits empty local DB | Persists on host |
| `gbrain sync` | Falls into PGLite migrations | Refuses with `gbrain remote ping` hint |
| `gbrain remote doctor` checks | 4 (config, oauth, discovery, smoke) | 5 (+ scope-probe with pinpoint remediation) |
| Identity feedback per command | None | `[thin-client → host:3131 · brain: 102k pages, 265k chunks · v0.31.1]` |

Per-call latency adds ~50-200ms warm (token cache hit), ~250-500ms cold (token mint).
Documented; users who want sub-50ms loops should run on the host.

### What this means for thin-client users

A thin-client `gbrain` install is now functionally substitutable for a local install
on the read+write+admin op surface. Agents get the topology signal up-front via the
identity banner; humans get pinpoint hints when something is genuinely local-only.
The OAuth-scope-mismatch class of failures (v0.29.2/v0.30.0 thin-clients registered
with `read+write` only and now hitting `gbrain stats`) surfaces during
`gbrain remote doctor` instead of mid-command, with an exact remediation:

`On the host: gbrain auth register-client <name> --grant-types client_credentials --scopes read,write,admin`

## To take advantage of v0.31.1

`gbrain upgrade` should do this automatically. If it didn't, or if `gbrain remote doctor`
warns about the new `oauth_client_scopes_probe` check:

1. **On the THIN CLIENT** — re-run doctor to surface scope mismatches:
```bash
gbrain remote doctor
```
The new `oauth_client_scopes_probe` check reports per-tier status. If admin is
missing, the check tells you the exact host-side command to run.

2. **On the HOST (if doctor warned about admin scope)** — re-register your thin-client's
OAuth client with broader scopes:
```bash
gbrain auth register-client <name> --grant-types client_credentials --scopes read,write,admin
```
Then update the thin-client's `~/.gbrain/config.json` `remote_mcp.oauth_client_id`
and (env-var or config) `oauth_client_secret` with the new credentials.

3. **Verify routing works** — run a representative read op that previously returned zero:
```bash
gbrain search "anything you know is in the brain"
gbrain stats
```
Both should now show real numbers and an identity banner like
`[thin-client → wintermute:3131 · brain: 102k pages, 265k chunks · v0.31.1]`.

4. **If any step fails or the numbers look wrong**, file an issue at
https://github.com/garrytan/gbrain/issues with `gbrain remote doctor` output.

### Itemized changes

#### Routing
- New `THIN_CLIENT_REFUSE_HINTS` table in `src/cli.ts` with pinpoint hints for every
refused command (sync/embed/extract/migrate/repair-jsonb/integrity/serve/dream/
orphans/transcripts/storage/takes/sources). Hints name the closest remote path
(e.g. `gbrain remote ping`) or specific MCP tools (e.g. `find_orphans`).
- New `runThinClientRouted` in cli.ts hooks INTO the existing op-dispatch path
(CDX-1: no parallel module). Every non-localOnly op routes through `callRemoteTool`
on thin-client installs.
- CLI-only commands with MCP equivalents (`think`, `salience`, `anomalies`,
`graph-query`) get per-command thin-client routing branches.
- ENG-2 renderer parity: local-engine path runs `JSON.parse(JSON.stringify(result))`
before formatting so renderers see the same shape on both paths.

#### New ops
- `get_brain_identity` (read scope, banner-only): cheap counter packet
`{version, engine, page_count, chunk_count, last_sync_iso}` for the thin-client
identity banner.

#### Banner
- Identity banner prints to stderr before each routed command. 60s TTL in-memory
cache keyed by mcp_url. Suppression: `--quiet`, `GBRAIN_NO_BANNER=1`, non-TTY
default (override with `GBRAIN_BANNER=1`).

#### Doctor
- New `oauth_client_scopes_probe` check (CDX-5) in `gbrain remote doctor`. Surfaces
v0.29.2/v0.30.0 thin-clients without admin scope BEFORE they hit
`gbrain stats`/`gbrain history` mid-command.

#### Error handling
- Hardened `callRemoteTool` (CDX-4): all transport errors normalized to
`RemoteMcpError` with stable `reason` union and `kind`/`code` sub-tags on detail.
Dispatcher's exhaustive TS `never` switch produces canned, actionable messages.
- New `--timeout=Ns` global flag (ENG-4); SIGINT aborts via AbortController.

#### Tests
- New `test/get-brain-identity.test.ts` (9), `test/oauth-scope-probe.test.ts` (8).
- Updated `test/cli-options.test.ts` (+8 --timeout cases, 28 total).
- Updated `test/cli-dispatch-thin-client.test.ts` (14 cases, refreshed for new
pinpoint-hint format).

### For contributors
- Plan + decision history at `~/.claude/plans/how-to-make-mcp-iterative-liskov.md`.
Records 6 CEO-review decisions (D1-D6), 5 eng-review decisions (ENG-1..5), and
6 codex outside-voice decisions (CDX-1..6). Codex's #1+#8 forced an architecture
rewrite (no parallel `src/core/thin-client/` module; route inline in cli.ts).
Codex's #2+#7 forced honest framing (no false "Liskov substitutability" claim
— server intentionally treats remote callers differently for `think.--save`/
`--take` and `put_page` auto-link, both trust-boundary policy decisions).
Codex's #5 caught a false premise in the error-model design.
- Per-subcommand thin-client routing for `takes` and `sources` is a v0.31.x
follow-up TODO. v0.31.1 refuses both at the top level with hints pointing at
MCP tools.

## [0.31.0] - 2026-05-08

**Hot memory ships. Your brain remembers what you said today, across sessions.**
Expand Down
57 changes: 57 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,63 @@ gbrain-evals consumes: `gbrain/engine`, `gbrain/types`, `gbrain/operations`,
`gbrain/extract`. Removing any of these is a breaking change for the
gbrain-evals consumer.

## Thin-client routing (v0.31.1, Issue #734)

`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.ts` — `callRemoteTool(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.ts` — `parseGlobalFlags` 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.ts` — `gbrain 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/operations.ts` — `get_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.

## Commands

Run `gbrain --help` or `gbrain --tools-json` for full command reference.
Expand Down
48 changes: 48 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
# TODOS

## Thin-client mode follow-ups (v0.31.1, Issue #734)

- [ ] **v0.31.x: routed-call timing telemetry.** `GBRAIN_TIMING=1` prints
`token_mint=Xms http=Yms server=Zms total=Wms` per routed MCP call.
Audit log at `~/.gbrain/audit/routed-calls-YYYY-Www.jsonl`. Cherry-pick
C from #734 plan; deferred from v0.31.1 to keep scope tight.

- [ ] **v0.31.2: job-submission routing for `gbrain dream` etc.** Route
long-running ops (`dream`, `embed --stale`, `extract`) via `submit_job`
+ poll, mirroring the existing `gbrain remote ping` autopilot-cycle
pattern. Cherry-pick D from #734 plan. Adds a thin-client async-job
render layer (progress events + spinner).

- [ ] **Per-subcommand thin-client routing for `takes` and `sources`.**
CDX-2 audit identified the READ subcommands (`takes_list`, `takes_search`,
`sources_list`, `sources_status`) as routable; mutate subcommands edit
local files. v0.31.1 refuses both at the top level with hints. Split
is a v0.31.x release.

- [ ] **Privacy decision: lift `localOnly: true` on `get_recent_transcripts`?**
Raw chat exports leaving the host is a real tradeoff. Needs explicit
per-token scope (`scope: 'transcripts'`) and consent UX. Out of v0.31.1.

- [ ] **Trust-boundary policy review for remote-caller gates.** Server
intentionally disables `think.--save`/`--take` for remote callers
(operations.ts:1103-1135) and skips `put_page` auto-link/auto-timeline
for remote callers without `trustedWorkspace` (operations.ts:434-451).
Subagent-isolation reasons; blocks full thin-client parity. Policy
decision, not a routing fix.

- [ ] **v0.32.0: flip `gbrain auth register-client` default scope from
`read` to `read,write,admin`.** Breaking for existing read-only scrapers;
ship deprecation warning in v0.31.x. The v0.31.1 `oauth_client_scopes_probe`
doctor check surfaces the gap with pinpoint remediation in the meantime.

- [ ] **v0.31.x: cross-process OAuth token cache at
`~/.gbrain/oauth-token-cache.json`.** Cuts ~200ms cold-start cost for
shell-loop usage on thin-client installs. Today the in-memory cache is
per-process; every `gbrain` invocation pays a fresh token mint.

- [ ] **v0.31.x: parity test (`test/thin-client-parity.test.ts`).** Plan
called for ~400 LOC byte-equal stdout assertions for 12+ ops via an
in-process MCP server pointed at the same PGLite as the local-engine
path. Harder than expected because it needs MCP server setup that the
current test infrastructure doesn't expose. v0.31.1 ships without it;
ENG-2's JSON-shape normalization + per-command test coverage is the
interim guard.

## LongMemEval benchmark follow-ups (v0.28.12)

### Closed: full 500-question 4-adapter run published
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.31.0
0.31.1
57 changes: 57 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,63 @@ gbrain-evals consumes: `gbrain/engine`, `gbrain/types`, `gbrain/operations`,
`gbrain/extract`. Removing any of these is a breaking change for the
gbrain-evals consumer.

## Thin-client routing (v0.31.1, Issue #734)

`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.ts` — `callRemoteTool(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.ts` — `parseGlobalFlags` 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.ts` — `gbrain 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/operations.ts` — `get_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.

## Commands

Run `gbrain --help` or `gbrain --tools-json` for full command reference.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gbrain",
"version": "0.31.0",
"version": "0.31.1",
"description": "Postgres-native personal knowledge brain with hybrid RAG search",
"type": "module",
"main": "src/core/index.ts",
Expand Down
Loading
Loading