Skip to content

gstack-gbrain-sync: sources list --json shape mismatch crashes /sync-gbrain --full on gbrain ≥ 0.18.0 #1609

@pennyultima

Description

@pennyultima

Repro

$ gstack --version  # 1.40.0.0
$ gbrain --version  # 0.33.1.0 (any ≥ 0.18.0)
$ /sync-gbrain --full
[gbrain-sync] mode=full engine=supabase
[gbrain-exec] seeded DATABASE_URL from /Users/x/.gbrain/config.json
gstack-gbrain-sync fatal: list.find is not a function. (In 'list.find((s) => s.id === sourceId)', 'list.find' is undefined)

Root cause

bin/gstack-gbrain-sync.ts:291sourceLocalPath():

export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
  const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(
    ["sources", "list", "--json"],
    { baseEnv: env },
  );
  if (!list) return null;
  const found = list.find((s) => s.id === sourceId);  // ← crashes
  return found?.local_path ?? null;
}

The function types the response as Array<...> but gbrain sources list --json has returned {sources: [...]} since gbrain v0.18.0 (commit 90c5d93, PR #337, "multi-source brains," 2026-04-22).

Reference, gbrain/src/commands/sources.ts:202:

console.log(JSON.stringify({ sources: entries }, null, 2));

Why it didn't show up sooner

sourceLocalPath is only called by runCodeImport, which runs on /sync-gbrain --full. Incremental sync (the hourly LaunchAgent path via gstack-brain-sync --once) doesn't touch it, so users whose code-stage reindex hasn't fired never see the crash.

Suggested patch (4 lines)

export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
  const raw = execGbrainJson<
    | Array<{ id: string; local_path?: string }>
    | { sources?: Array<{ id: string; local_path?: string }> }
  >(["sources", "list", "--json"], { baseEnv: env });
  if (!raw) return null;
  // gbrain v0.18.0+ returns {sources: [...]}; older versions returned a raw array.
  const list = Array.isArray(raw) ? raw : raw.sources ?? [];
  const found = list.find((s) => s.id === sourceId);
  return found?.local_path ?? null;
}

Defensive against both shapes, so it works on any gbrain version.

Suggested test

A contract test that shells out to a real gbrain sources list --json (against a tmp engine) would have caught this. The current test suite must be using a fixture that returns the raw-array shape, since the function would otherwise fail in unit tests too. Worth adding test/contract/gbrain-cli.test.ts that runs against a real gbrain binary and red-flags any new shape divergence in CI.

Environment

  • macOS 15.x, gstack v1.40.0.0
  • gbrain v0.33.1.0 (also reproduces on any gbrain ≥ 0.18.0 based on shape history)
  • Mode: local-stdio, engine: Supabase postgres
  • Repro path: /sync-gbrain --full on a fully-set-up brain

Happy to send a PR if helpful.

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