{"record":{"id":"e56f2744e2894a1a","repo":"garrytan/gstack","slug":"refusing-drift-re-register-of-id-decision-re","errorCode":null,"errorMessage":"refusing drift re-register of ${id}: ${decision.reason}","messagePattern":"refusing drift re-register of (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/gbrain-sources.ts","lineNumber":224,"sourceCode":"      console.error(\n        `[gbrain-sources] drift: ${id} registered at ${state.registered_path} -> re-registering at ${path}`,\n      );\n\n      // #1734: this remove deletes the source's pages/chunks/embeddings, so it\n      // runs only behind the same data-loss guards as the orchestrator's\n      // safeSourcesRemove(). A refusal is FATAL here (not best-effort): without\n      // the remove the add cannot proceed, and returning changed=false would\n      // silently hide the drifted registration.\n      const ap = detectAutopilot(env ?? process.env, options.autopilotProbe ?? {});\n      if (ap.active) {\n        throw new Error(\n          `refusing drift re-register of ${id}: autopilot active (${ap.signal}). ` +\n            `Stop autopilot, then re-run /sync-gbrain.`,\n        );\n      }\n      const decision = decideSourceRemove(id, env ?? process.env, options.removeDecision ?? {});\n      if (!decision.allow) {\n        throw new Error(`refusing drift re-register of ${id}: ${decision.reason}`);\n      }\n\n      const rm = spawnSync(\n        \"gbrain\",\n        [\"sources\", \"remove\", id, \"--yes\", \"--confirm-destructive\", ...decision.extraArgs],\n        {\n          encoding: \"utf-8\",\n          timeout: 30_000,\n          env,\n          shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows\n        },\n      );\n      if (rm.status !== 0) {\n        throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);\n      }\n    }\n\n    // Add.","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/lib/gbrain-sources.ts#L206-L242","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: sync refuses because removeDecision denies the id\nawait ensureSourceRegistered(id, newPath, { reregister_on_drift: true });\n\n// after: explicitly allow this drifted id through the remove decision\nawait ensureSourceRegistered(id, newPath, {\n  reregister_on_drift: true,\n  removeDecision: { allowIds: [id], requireConfirm: true },\n});","handlingStrategy":"validation","validationCode":"// Before calling ensureSourceRegistered for a drifted source, pre-check the remove decision.\nimport { decideSourceRemove, detectAutopilot } from './gbrain-guards';\n\nfunction canReregister(id: string, env = process.env): { ok: true } | { ok: false; reason: string } {\n  const ap = detectAutopilot(env, {});\n  if (ap.active) return { ok: false, reason: `autopilot active (${ap.signal})` };\n  const d = decideSourceRemove(id, env, {});\n  if (!d.allow) return { ok: false, reason: d.reason };\n  return { ok: true };\n}\n\n// usage\nconst v = canReregister(id);\nif (!v.ok) { console.warn(`skipping ${id}: ${v.reason}`); return; }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["gbrain","sync","drift","destructive-op","configuration"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}