fix: detect PgBouncer transaction-mode pooler and set GBRAIN_PREPARE=true (#1435)

When gbrain connects through a PgBouncer transaction-mode pooler (port
6543), it auto-disables prepared statements. This breaks `gbrain search`
silently — the /sync-gbrain capability check fails and the GBrain Search
Guidance block never gets written to CLAUDE.md.

Three-layer fix:

1. **lib/gbrain-exec.ts** — `buildGbrainEnv()` now detects port 6543 in
   the effective DATABASE_URL and sets `GBRAIN_PREPARE=true` in the env
   passed to every gbrain spawn. This is the single chokepoint — all
   gstack gbrain invocations inherit the fix. Caller can opt out with
   `GBRAIN_PREPARE=false`.

2. **sync-gbrain/SKILL.md{,.tmpl}** — capability check now exports
   `GBRAIN_PREPARE=true` explicitly and retries search up to 3x with 1s
   delay for async index propagation under connection pooling.

3. **bin/gstack-gbrain-detect** — surfaces `gbrain_pooler_mode` field
   ("transaction" | "session" | null) in the preamble probe JSON so
   /setup-gbrain and /sync-gbrain can advise users about pooler state.

Closes #1435

Built with [ClosedLoop.AI](https://closedloop.ai) | [GitHub](https://github.com/closedloop-ai/claude-plugins)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mikeangstadt
2026-05-18 16:57:51 -05:00
committed by Garry Tan
parent c427340fce
commit db2ed599a3
5 changed files with 169 additions and 19 deletions
+47 -5
View File
@@ -54,6 +54,26 @@ export interface BuildGbrainEnvOptions {
announce?: boolean;
}
/**
* Detect whether a DATABASE_URL targets a PgBouncer transaction-mode pooler.
*
* Supabase transaction-mode poolers conventionally run on port 6543 at
* `*.pooler.supabase.com`. When gbrain connects through one of these, it
* auto-disables prepared statements — but search requires them (#1435).
* Returns `true` when the URL looks like a transaction-mode pooler so the
* caller can set `GBRAIN_PREPARE=true` to re-enable prepared statements.
*/
export function isTransactionModePooler(url: string): boolean {
try {
// DATABASE_URLs use postgresql:// scheme which URL() doesn't natively
// parse host/port from, so swap to http:// for reliable parsing.
const parsed = new URL(url.replace(/^postgres(ql)?:\/\//, "http://"));
return parsed.port === "6543";
} catch {
return false;
}
}
/**
* Build an env dict with DATABASE_URL seeded from
* `${GBRAIN_HOME:-$HOME/.gbrain}/config.json`. Returns the base env
@@ -63,6 +83,11 @@ export interface BuildGbrainEnvOptions {
* - the config has no `database_url`,
* - the caller already set DATABASE_URL to the same value.
*
* When the effective DATABASE_URL targets a PgBouncer transaction-mode
* pooler (port 6543), sets `GBRAIN_PREPARE=true` so gbrain re-enables
* prepared statements needed for search (#1435). Caller can override
* with `GBRAIN_PREPARE=false` in the base env.
*
* Always returns a fresh object — mutating the returned env never
* affects the caller's env. Tests assert on effective values, not
* object identity.
@@ -84,14 +109,31 @@ export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.Process
return out;
}
if (!cfg.database_url) return out;
if (baseEnv.DATABASE_URL === cfg.database_url) return out;
const hadCaller = baseEnv.DATABASE_URL !== undefined;
out.DATABASE_URL = cfg.database_url;
if (opts.announce) {
const note = hadCaller ? " (overrode value from caller env / .env.local)" : "";
process.stderr.write(`[gbrain-exec] seeded DATABASE_URL from ${configPath}${note}\n`);
const alreadyMatch = baseEnv.DATABASE_URL === cfg.database_url;
if (!alreadyMatch) {
out.DATABASE_URL = cfg.database_url;
if (opts.announce) {
const note = hadCaller ? " (overrode value from caller env / .env.local)" : "";
process.stderr.write(`[gbrain-exec] seeded DATABASE_URL from ${configPath}${note}\n`);
}
}
// PgBouncer transaction-mode pooler detection (#1435): when the effective
// DATABASE_URL targets port 6543 (Supabase transaction-mode convention),
// gbrain auto-disables prepared statements — but search needs them.
// Set GBRAIN_PREPARE=true unless the caller explicitly opted out.
const effectiveUrl = out.DATABASE_URL || cfg.database_url;
if (effectiveUrl && !out.GBRAIN_PREPARE && isTransactionModePooler(effectiveUrl)) {
out.GBRAIN_PREPARE = "true";
if (opts.announce) {
process.stderr.write(
`[gbrain-exec] set GBRAIN_PREPARE=true (port 6543 transaction-mode pooler detected)\n`,
);
}
}
return out;
}