garrytan/gstack · error · Error
refusing drift re-register of ${id}: ${decision.reason}
Error message
refusing drift re-register of ${id}: ${decision.reason} What it means
Thrown during /sync-gbrain when a registered source has drifted to a new path and the code must remove-then-re-add it, but decideSourceRemove() refuses the destructive remove. The refusal is fatal (not best-effort) because without the remove the add cannot proceed, and silently returning changed=false would hide the drifted registration. The decision.reason string carries the specific guard that blocked it.
Source
Thrown at lib/gbrain-sources.ts:224
console.error(
`[gbrain-sources] drift: ${id} registered at ${state.registered_path} -> re-registering at ${path}`,
);
// #1734: this remove deletes the source's pages/chunks/embeddings, so it
// runs only behind the same data-loss guards as the orchestrator's
// safeSourcesRemove(). A refusal is FATAL here (not best-effort): without
// the remove the add cannot proceed, and returning changed=false would
// silently hide the drifted registration.
const ap = detectAutopilot(env ?? process.env, options.autopilotProbe ?? {});
if (ap.active) {
throw new Error(
`refusing drift re-register of ${id}: autopilot active (${ap.signal}). ` +
`Stop autopilot, then re-run /sync-gbrain.`,
);
}
const decision = decideSourceRemove(id, env ?? process.env, options.removeDecision ?? {});
if (!decision.allow) {
throw new Error(`refusing drift re-register of ${id}: ${decision.reason}`);
}
const rm = spawnSync(
"gbrain",
["sources", "remove", id, "--yes", "--confirm-destructive", ...decision.extraArgs],
{
encoding: "utf-8",
timeout: 30_000,
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
},
);
if (rm.status !== 0) {
throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);
}
}
// Add.View on GitHub (pinned to 94993f7401)
Solutions
- Read decision.reason in the thrown message and address that specific guard (e.g. set the env var it asks for, or allow the id in removeDecision.extraArgs).
- If the drift is unintended, move the source back to its registered_path so status becomes 'match' and no remove is needed.
- Stop any active autopilot process and re-run /sync-gbrain (autopilot refusal is a separate but adjacent guard).
- If the source is genuinely stale and safe to drop, pre-remove it manually with `gbrain sources remove <id> --yes --confirm-destructive` so the sync add runs cleanly.
Example fix
// before: sync refuses because removeDecision denies the id
await ensureSourceRegistered(id, newPath, { reregister_on_drift: true });
// after: explicitly allow this drifted id through the remove decision
await ensureSourceRegistered(id, newPath, {
reregister_on_drift: true,
removeDecision: { allowIds: [id], requireConfirm: true },
}); Defensive patterns
Strategy: validation
Validate before calling
// Before calling ensureSourceRegistered for a drifted source, pre-check the remove decision.
import { decideSourceRemove, detectAutopilot } from './gbrain-guards';
function canReregister(id: string, env = process.env): { ok: true } | { ok: false; reason: string } {
const ap = detectAutopilot(env, {});
if (ap.active) return { ok: false, reason: `autopilot active (${ap.signal})` };
const d = decideSourceRemove(id, env, {});
if (!d.allow) return { ok: false, reason: d.reason };
return { ok: true };
}
// usage
const v = canReregister(id);
if (!v.ok) { console.warn(`skipping ${id}: ${v.reason}`); return; } Prevention
- Keep source paths stable so drift does not occur — move directories via the gbrain rename command, not the filesystem.
- Document the removeDecision allowlist in the repo so new sources are added to it at the same time.
- Run /sync-gbrain from a known-clean state (no autopilot) so the guards are predictable.
- Treat perpetual drift as a signal to adopt the reindex-in-place design rather than perpetual drop+rebuild.
When it happens
Trigger: Calling ensureSourceRegistered() (or the /sync-gbrain flow) for a source id whose probed registered_path differs from the desired path, while decideSourceRemove() returns allow=false — e.g. GBRAIN_REQUIRE_CONFIRM env gating, a missing --confirm-destructive allowance, or a removeDecision override that denies the id. Autopilot is inactive (a different, earlier throw covers autopilot-active).
Common situations: A CI/automation environment that sets restrictive remove-decision env vars; a teammate who added a new source guard that denies an existing drifted id; running /sync-gbrain right after moving a source directory on disk without updating the remove allowlist; a stale gbrain config that perpetually marks a source as protected.
Related errors
- refusing drift re-register of ${id}: autopilot active (${ap.
- gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout
- commitSkill: tier "${opts.tier}" has no resolved path.
- commitSkill: a skill named "${opts.name}" already exists at
- skill command requires daemonPort in MetaCommandOpts (server
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/e56f2744e2894a1a.
Report an issue: GitHub.