Repro
Two machines, identical home-dir layout (common with chezmoi / synced dotfiles).
Machine A:
cd /home/cloud-user/github/remodspace
/sync-gbrain
# Registers source gstack-code-pace-a753493f-b33352
# with local_path=/home/cloud-user/github/remodspace
Machine B (same shared brain DB, e.g. Aiven Postgres / federated Supabase):
cd /home/cloud-user/github/remodspace # same path, may not be a git repo here
gbrain sync
# Error: Not a git repository: /home/cloud-user/github/remodspace.
# GBrain sync requires a git-initialized repo.
Even when the path is a git repo on machine B, the second machine's /sync-gbrain overwrites the source row's local_path to its value — and machine A then errors symmetrically. Last-writer-wins.
Root cause
bin/gstack-gbrain-sync.ts:176-186 (deriveCodeSourceId):
function deriveCodeSourceId(repoPath: string): string {
const pathHash = createHash(\"sha1\").update(repoPath).digest(\"hex\").slice(0, 8);
const remote = canonicalizeRemote(originUrl());
if (remote) {
const segs = remote.split(\"/\").filter(Boolean);
const slugSource = segs.slice(-2).join(\"-\");
return constrainSourceId(\"gstack-code\", \`\${slugSource}-\${pathHash}\`);
}
const base = repoPath.split(\"/\").pop() || \"repo\";
return constrainSourceId(\"gstack-code\", \`\${base}-\${pathHash}\`);
}
pathHash = sha1(repoPath).slice(0, 8) is a function of the absolute filesystem path only. No machine/host identifier. Two machines with identical layouts produce identical source IDs.
The federation model in the v1.29.0.0 changelog ("Conductor worktrees of the same repo coexist as separate sources in the same gbrain DB instead of stomping on each other") works correctly within one machine because Conductor worktrees live at different paths on the same host. It breaks across machines because the path is the same on both.
How the error surfaces
gbrain's source resolver (gbrain/src/core/source-resolver.ts:101-115, step 4) does longest-prefix match against sources.local_path to pick a source for bare gbrain sync. On machine B inside the path-collision directory:
.gbrain-source pin doesn't exist (it's gitignored, so it doesn't travel with the repo)
- Step 4 fires: cwd
/home/cloud-user/github/remodspace exactly matches the source's local_path
- gbrain reads that source's
local_path (same path), validates .git exists there
- If machine B doesn't have it as a git repo → `Not a git repository` error
- Auto-recovery via re-clone (
gbrain/src/commands/sync.ts:340-360) is gated on config.remote_url, which /sync-gbrain doesn't store (it registers with --path, not --url)
Impact
- Identical-layout home dirs (common with chezmoi, ansible, single-user multi-host fleets) silently collide
- Bare
gbrain sync errors out cryptically — users don't know why their working clone isn't recognized
- Even when it doesn't error, last-machine-to-sync wins for
local_path, so the other machine starts pointing at the wrong path
- Cross-source search results are correct (federation reads pages from the brain by source_id), but only one machine can author against the source at a time
Fix shape
Make the source ID include a host identifier so two machines can't collide on the same path:
import { hostname } from \"os\";
function deriveCodeSourceId(repoPath: string): string {
// Hash both host and absolute path. Truncated to fit gbrain's 32-char id cap.
const hostPathKey = \`\${hostname()}::\${repoPath}\`;
const hostPathHash = createHash(\"sha1\").update(hostPathKey).digest(\"hex\").slice(0, 8);
// …rest unchanged, with hostPathHash replacing pathHash
}
Open questions for the design:
- Migration: existing brains have path-only-hashed IDs. New format would create parallel sources rather than reusing the existing one. Either (a) leave legacy ones alone and let them age out, (b) add a one-shot migration that re-keys with
\hostname()`-derived` hashes, or (c) make legacy IDs continue to resolve via a fallback rule.
- Hostname stability: machines do get renamed. A machine ID file (
/etc/machine-id on Linux, equivalent elsewhere) is stabler than hostname() but more platform-specific. Worth picking deliberately.
- Conductor worktrees: still need to coexist on a single host. Including hostname doesn't break this because the per-worktree path differs within a host.
- Stomping the existing field: the source row's
local_path is still single-valued; this fix prevents collisions but doesn't change that semantics. Two machines using different paths for the same logical repo will still get two sources, which is the right behavior.
Alternative path: store remote_url on registration so gbrain's auto-clone-on-missing kicks in. That doesn't fix the collision but masks the most common error path. Doesn't help when machines have legitimately different content at the same path (e.g. one is a feature branch checkout, one is main).
Environment
Happy to test a fix against this setup.
Repro
Two machines, identical home-dir layout (common with chezmoi / synced dotfiles).
Machine A:
Machine B (same shared brain DB, e.g. Aiven Postgres / federated Supabase):
Even when the path is a git repo on machine B, the second machine's
/sync-gbrainoverwrites the source row'slocal_pathto its value — and machine A then errors symmetrically. Last-writer-wins.Root cause
bin/gstack-gbrain-sync.ts:176-186(deriveCodeSourceId):pathHash = sha1(repoPath).slice(0, 8)is a function of the absolute filesystem path only. No machine/host identifier. Two machines with identical layouts produce identical source IDs.The federation model in the v1.29.0.0 changelog ("Conductor worktrees of the same repo coexist as separate sources in the same gbrain DB instead of stomping on each other") works correctly within one machine because Conductor worktrees live at different paths on the same host. It breaks across machines because the path is the same on both.
How the error surfaces
gbrain's source resolver (
gbrain/src/core/source-resolver.ts:101-115, step 4) does longest-prefix match againstsources.local_pathto pick a source for baregbrain sync. On machine B inside the path-collision directory:.gbrain-sourcepin doesn't exist (it's gitignored, so it doesn't travel with the repo)/home/cloud-user/github/remodspaceexactly matches the source'slocal_pathlocal_path(same path), validates.gitexists theregbrain/src/commands/sync.ts:340-360) is gated onconfig.remote_url, which/sync-gbraindoesn't store (it registers with--path, not--url)Impact
gbrain syncerrors out cryptically — users don't know why their working clone isn't recognizedlocal_path, so the other machine starts pointing at the wrong pathFix shape
Make the source ID include a host identifier so two machines can't collide on the same path:
Open questions for the design:
\hostname()`-derived` hashes, or (c) make legacy IDs continue to resolve via a fallback rule./etc/machine-idon Linux, equivalent elsewhere) is stabler thanhostname()but more platform-specific. Worth picking deliberately.local_pathis still single-valued; this fix prevents collisions but doesn't change that semantics. Two machines using different paths for the same logical repo will still get two sources, which is the right behavior.Alternative path: store
remote_urlon registration so gbrain's auto-clone-on-missing kicks in. That doesn't fix the collision but masks the most common error path. Doesn't help when machines have legitimately different content at the same path (e.g. one is a feature branch checkout, one is main).Environment
cloud-user, chezmoi-managed homeHappy to test a fix against this setup.