Skip to content

fix(gstack-gbrain-sync): engine always "unknown" when gbrain ≥0.25 is unhealthy — silently breaks /sync-gbrain for all Supabase users #1415

@shivasymbl

Description

@shivasymbl

What's happening

`/sync-gbrain` (and any gstack skill that calls `detectEngineTier()`) always reports `engine=unknown` for anyone running gbrain ≥0.25 against a Supabase brain that has any unhealthy check. That covers basically everyone on Supabase — because a fresh install always has at least one warning (resolver_health, or a pending migration) until everything is perfectly tuned.

When engine is unknown, the orchestrator logs `[gbrain-sync] mode=X engine=unknown` and skips all three sync stages silently. No error. No hint that anything is wrong. You just... don't get a brain sync.

Root cause

It's two things hitting at the same time, which is why it's subtle.

Problem 1: execSync throws on non-zero exit, swallowing the JSON

`freshDetectEngineTier()` in `lib/gstack-memory-helpers.ts` (line 252) does this:

```typescript
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
const parsed = JSON.parse(out);
```

`gbrain doctor` exits with code `1` whenever health_score < 100. `execSync` throws on non-zero exit — the stdout never reaches `out`. The catch block returns `{ engine: "unknown" }` immediately. The JSON that gbrain wrote to stdout is gone.

Problem 2: gbrain ≥0.25 changed doctor's output format

Even if the exit code were zero, it wouldn't help. gbrain ≥0.25 switched to `schema_version:2` for doctor output, which dropped the top-level `engine` field entirely. The old format had `parsed.engine === "pglite"` or `parsed.engine === "supabase"`. The new format looks like:

```json
{
"schema_version": 2,
"status": "unhealthy",
"health_score": 70,
"checks": [...]
}
```

No `engine` key. `parsed?.engine` is `undefined`. Falls straight to `"unknown"`.

Both problems need fixing. Either one alone would still break things.

Repro

  1. Install gbrain ≥0.25, connect to Supabase
  2. Make sure at least one doctor check is non-green (resolver_health warning is always present on a fresh install — this is basically always true)
  3. Run `/sync-gbrain` or call `detectEngineTier()` directly:

```bash
bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --dry-run

Output: [gbrain-sync] mode=dry-run engine=unknown

```

Or inline:
```bash
cd ~/.claude/skills/gstack && bun -e "
const { detectEngineTier } = await import('./lib/gstack-memory-helpers.ts');
console.log(detectEngineTier());
"

{ engine: 'unknown', ... }

```

Environment:

  • gstack v1.31.0.0
  • gbrain v0.31.3
  • Supabase Postgres brain (schema_version:2 doctor output)
  • macOS 26.4.1

Fix

Two changes to `freshDetectEngineTier()`:

  1. Catch the `execSync` throw and read stdout off the error object (Node.js puts it there when `stdio: 'pipe'` — the default for execSync)
  2. When `parsed?.engine` is still undefined after that (schema_version:2 case), fall back to reading `~/.gbrain/config.json`

```typescript
function freshDetectEngineTier(): EngineDetect {
const now = Date.now();
let parsed: Record<string, unknown> | null = null;
try {
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
parsed = JSON.parse(out);
} catch (err: unknown) {
// execSync throws on non-zero exit; stdout is still on the error object
try {
const stdout = (err as { stdout?: string })?.stdout ?? "";
if (stdout) parsed = JSON.parse(stdout);
} catch { /* unparseable stdout — stay null */ }
}

// gbrain >=0.25 uses schema_version:2 which dropped the top-level engine
// field. Fall back to ~/.gbrain/config.json when doctor doesn't provide it.
let engine: EngineTier = parsed?.engine === "supabase" ? "supabase" : parsed?.engine === "pglite" ? "pglite" : "unknown";
if (engine === "unknown") {
try {
const configPath = join(homedir(), ".gbrain", "config.json");
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
if (cfg?.engine === "pglite") engine = "pglite";
else if (cfg?.engine === "postgres" || cfg?.database_url) engine = "supabase";
} catch { /* config unreadable — stay unknown */ }
}

return {
engine,
supabase_url: parsed?.supabase_url as string | undefined,
detected_at: now,
schema_version: 1,
};
}
```

Tested on gstack v1.31.0.0 + gbrain v0.31.3 + Supabase. Before the patch: `engine=unknown` on every invocation. After: `engine=supabase`.

Why this matters

Everyone using `/sync-gbrain` with Supabase has been getting silent no-ops. There's no error — just a dry-run-like outcome where all stages skip. The brain never syncs. This is especially painful because the resolver_health warning (which triggers non-zero exit from doctor) is present on basically every install.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions