mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 07:40:09 +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:
@@ -15,7 +15,7 @@ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
import { buildGbrainEnv } from "../lib/gbrain-exec";
|
||||
import { buildGbrainEnv, isTransactionModePooler } from "../lib/gbrain-exec";
|
||||
|
||||
describe("buildGbrainEnv", () => {
|
||||
let home: string;
|
||||
@@ -117,4 +117,74 @@ describe("buildGbrainEnv", () => {
|
||||
const result = buildGbrainEnv({ baseEnv });
|
||||
expect(result.DATABASE_URL).toBe("postgresql://gbrain/db");
|
||||
});
|
||||
|
||||
// --- GBRAIN_PREPARE auto-detection (#1435) ---
|
||||
|
||||
it("sets GBRAIN_PREPARE=true when DATABASE_URL targets port 6543 (transaction-mode pooler)", () => {
|
||||
const poolerUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres";
|
||||
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: poolerUrl }));
|
||||
const baseEnv = { HOME: home };
|
||||
const result = buildGbrainEnv({ baseEnv });
|
||||
expect(result.DATABASE_URL).toBe(poolerUrl);
|
||||
expect(result.GBRAIN_PREPARE).toBe("true");
|
||||
});
|
||||
|
||||
it("does not set GBRAIN_PREPARE when DATABASE_URL targets port 5432 (session-mode pooler)", () => {
|
||||
const sessionUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:5432/postgres";
|
||||
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: sessionUrl }));
|
||||
const baseEnv = { HOME: home };
|
||||
const result = buildGbrainEnv({ baseEnv });
|
||||
expect(result.GBRAIN_PREPARE).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not set GBRAIN_PREPARE for pglite (no port in URL)", () => {
|
||||
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: "postgresql://gbrain/db" }));
|
||||
const baseEnv = { HOME: home };
|
||||
const result = buildGbrainEnv({ baseEnv });
|
||||
expect(result.GBRAIN_PREPARE).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects caller's explicit GBRAIN_PREPARE=false (opt-out)", () => {
|
||||
const poolerUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres";
|
||||
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: poolerUrl }));
|
||||
const baseEnv = { HOME: home, GBRAIN_PREPARE: "false" };
|
||||
const result = buildGbrainEnv({ baseEnv });
|
||||
expect(result.GBRAIN_PREPARE).toBe("false");
|
||||
});
|
||||
|
||||
it("sets GBRAIN_PREPARE even when caller DATABASE_URL already matches config on port 6543", () => {
|
||||
const poolerUrl = "postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres";
|
||||
writeFileSync(join(gbrainHome, "config.json"), JSON.stringify({ database_url: poolerUrl }));
|
||||
const baseEnv = { HOME: home, DATABASE_URL: poolerUrl };
|
||||
const result = buildGbrainEnv({ baseEnv });
|
||||
expect(result.GBRAIN_PREPARE).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTransactionModePooler", () => {
|
||||
it("returns true for Supabase transaction-mode pooler URL (port 6543)", () => {
|
||||
expect(isTransactionModePooler(
|
||||
"postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:6543/postgres"
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for session-mode pooler URL (port 5432)", () => {
|
||||
expect(isTransactionModePooler(
|
||||
"postgresql://postgres.abc:pw@aws-0-us-east-1.pooler.supabase.com:5432/postgres"
|
||||
)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for pglite-style URL (no port)", () => {
|
||||
expect(isTransactionModePooler("postgresql://gbrain/db")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unparseable URL", () => {
|
||||
expect(isTransactionModePooler("not-a-url")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles postgres:// scheme (without 'ql')", () => {
|
||||
expect(isTransactionModePooler(
|
||||
"postgres://postgres.abc:pw@host:6543/postgres"
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user