mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 15:50:11 +02:00
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:
+47
-5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user