Skip to content

fix(cli): route gbrain capture through put_page on thin-client installs#2266

Open
spinsirr wants to merge 1 commit into
garrytan:masterfrom
spinsirr:fix/capture-thin-client-routing
Open

fix(cli): route gbrain capture through put_page on thin-client installs#2266
spinsirr wants to merge 1 commit into
garrytan:masterfrom
spinsirr:fix/capture-thin-client-routing

Conversation

@spinsirr

Copy link
Copy Markdown

Summary

On a thin-client install (config has remote_mcp, no local database_url), gbrain capture "..." errors out:

No database URL: database_url is missing from config.
Fix: Run gbrain init --supabase or gbrain init --url

…even though gbrain put, gbrain whoami, and gbrain search all route remote correctly on the exact same config. Only capture breaks.

This violates the documented contract (README + CHANGELOG):

thin-client installs route through callRemoteTool('put_page', ...). Same UX both ways.

Reproduce

gbrain init --mcp-only \
  --issuer-url <url> --mcp-url <url>/mcp \
  --oauth-client-id <id> --oauth-client-secret <secret>
# creates a config with remote_mcp and NO database_url

gbrain capture "test"
# → No database URL: database_url is missing from config. ...

gbrain put inbox/x on the same config routes remote correctly (it only fails on reaching the server), proving the issue is specific to capture.

Root cause

capture is a CLI_ONLY command, so it is dispatched through handleCliOnly() rather than the shared-op routing seam in main() that sends non-localOnly ops through runThinClientRouted on thin-client installs.

handleCliOnly already has thin-client bypasses for status and eval whoknows (both pass engine=null and return before connecting) — but not for capture. So capture falls through to the unconditional connectEngine(), which throws No database URL from src/core/db.ts before capture's own thin-client branch (callRemoteTool('put_page', ...)) can ever run.

src/commands/capture.ts was already correct: runCapture accepts engine=null and guards every local-engine use behind isThinClient(cfg) (the source resolver at ~L426, the put_page-op fallback at ~L506). The only thing missing was the dispatch-time bypass so the local engine is never opened on a thin-client.

put / whoami / search work because they are registered ops (not CLI_ONLY) and hit the main() routing seam.

Fix

Add a thin-client bypass for capture in handleCliOnly, mirroring the existing status / eval whoknows guards:

if (command === 'capture') {
  const cfgPre = loadConfig();
  if (isThinClient(cfgPre)) {
    const { runCapture } = await import('./commands/capture.ts');
    await runCapture(null, args);
    return;
  }
}

capture is intentionally not added to THIN_CLIENT_REFUSED_COMMANDS because it can run remotely (the server exposes put_page).

Test

Adds a regression test to test/cli-dispatch-thin-client.test.ts that seeds a thin-client config, spawns gbrain capture, and asserts it never emits No database URL and instead surfaces the remote put_page path. The "remote put_page failed:" prefix is unique to capture's thin-client catch and cannot appear on the local-engine path, so it is precise proof of remote routing. Watched it fail before the fix (RED) and pass after (GREEN).

Verification

  • tsc --noEmit — clean
  • bun run verify (30 checks) — all green
  • bun test on cli-dispatch-thin-client, thin-client-routing-audit, capture-runcapture, commands/capture, ingestion/ingest-capture97 pass, 0 fail

Notes

  • Local installs are unchanged: the bypass is gated on isThinClient(cfg), so local-engine capture takes the existing path untouched.
  • src/commands/capture.ts is unchanged from v0.42.36 through current master, so this bug is present on master too.

…alls

On a thin-client install (config has `remote_mcp`, no `database_url`),
`gbrain capture "..."` failed with:

    No database URL: database_url is missing from config.
    Fix: Run gbrain init --supabase or gbrain init --url

even though `gbrain put`, `gbrain whoami`, and `gbrain search` all routed
remote correctly on the exact same config.

Root cause: `capture` is a CLI_ONLY command, so it is dispatched through
`handleCliOnly()` rather than the shared-op routing seam in `main()` that
sends non-localOnly ops through `runThinClientRouted` on thin-client
installs. `handleCliOnly` already has thin-client bypasses for `status`
and `eval whoknows` (both pass `engine=null` and return before connect),
but not for `capture` — so capture fell through to the unconditional
`connectEngine()`, which throws "No database URL" from src/core/db.ts
before capture's own thin-client branch (callRemoteTool('put_page', ...))
could ever run.

capture.ts itself was already correct: `runCapture` accepts `engine=null`
and guards every local-engine use behind `isThinClient(cfg)` (the source
resolver at ~L426, the put_page-op fallback at ~L506). The only thing
missing was the dispatch-time bypass so the engine is never opened.

Fix: add a thin-client bypass for `capture` in `handleCliOnly`, mirroring
the existing `status` / `eval whoknows` guards. `capture` is intentionally
NOT added to THIN_CLIENT_REFUSED_COMMANDS because it CAN run remotely (the
server exposes put_page) — matching the documented contract: "thin-client
installs route through callRemoteTool('put_page', ...). Same UX both ways."

Test: adds a regression test in test/cli-dispatch-thin-client.test.ts that
seeds a thin-client config, runs `gbrain capture`, and asserts it never
emits "No database URL" and instead surfaces the remote put_page path
(the "remote put_page failed:" prefix is unique to capture's thin-client
catch and cannot appear on the local-engine path).

Verified: `tsc --noEmit` clean; `bun run verify` (30 checks) all green;
cli-dispatch-thin-client + thin-client-routing-audit + capture-runcapture +
commands/capture + ingestion/ingest-capture suites pass (97 tests, 0 fail).

Co-Authored-By: Zypher Agent <zypher@corespeed.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant