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

  1. 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).
  2. If the drift is unintended, move the source back to its registered_path so status becomes 'match' and no remove is needed.
  3. Stop any active autopilot process and re-run /sync-gbrain (autopilot refusal is a separate but adjacent guard).
  4. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/e56f2744e2894a1a. Report an issue: GitHub.