{"record":{"id":"39e5bb85819d33be","repo":"garrytan/gstack","slug":"refusing-drift-re-register-of-id-autopilot-act","errorCode":null,"errorMessage":"refusing drift re-register of ${id}: autopilot active (${ap.signal}). Stop autopilot, then re-run /sync-gbrain.","messagePattern":"refusing drift re-register of (.+?): autopilot active \\((.+?)\\)\\. Stop autopilot, then re-run /sync-gbrain\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/gbrain-sources.ts","lineNumber":217,"sourceCode":"    // as \"source registration failed\" and aborts the whole /sync-gbrain code\n    // stage for any source that has drifted to a new path. This matches the\n    // flag the orchestrator's own safeSourcesRemove() already passes.\n    if (state.status === \"drift\") {\n      // Loud drift observability: if this line shows up on every sync for some\n      // environment, drift is perpetual there and the reindex-in-place design\n      // from #1985 should be promoted (drop+rebuild re-embeds the full index).\n      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        },","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/lib/gbrain-sources.ts#L199-L235","documentation":"Thrown by ensureSourceRegistered() in the drift branch when a source is registered at a different path than requested AND detectAutopilot reports autopilot active. Re-registering a drifted source requires a destructive remove (which deletes pages/chunks/embeddings) followed by re-add; running that while autopilot is indexing would corrupt the in-progress rebuild. The refusal is fatal by design — returning changed=false would silently hide the drift. The message names the id and the autopilot signal so the operator knows what to stop.","triggerScenarios":"probeSource returns status=match but the realpath-normalized path differs from the requested path (drift), and detectAutopilot(env).active is true. The function then throws before attempting the remove+add, instructing the operator to stop autopilot first.","commonSituations":"Repository moved on disk so the source path changed while gbrain still tracks the old location, and autopilot is mid-reindex; a CI machine clones to a temp dir each run causing perpetual drift; symlink re-resolved to a different absolute path; autopilot left running from a previous session.","solutions":["Stop the autopilot process (the named signal tells you which), then re-run /sync-gbrain.","If drift is perpetual (every sync reports it), align the registered path with the actual path permanently — re-add at the canonical location once.","Verify detectAutopilot isn't reporting a stale signal (e.g. a leftover lockfile); clean it if so.","As a last resort, manually remove the drifted source via gbrain CLI after confirming autopilot is stopped."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { detectAutopilot } from './gbrain-guards';\nfunction assertAutopilotIdle(env: NodeJS.ProcessEnv = process.env): void {\n  const ap = detectAutopilot(env, {});\n  if (ap.active) throw new Error(`Stop autopilot (${ap.signal}) before re-registering sources.`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await ensureSourceRegistered(id, path, options);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('refusing drift re-register') && /autopilot active/.test(e.message)) {\n    // Stop autopilot, then re-run /sync-gbrain\n    throw new Error('Autopilot is running. Stop it, then re-run /sync-gbrain.');\n  }\n  throw e;\n}","preventionTips":["Always stop autopilot before re-running /sync-gbrain on a moved repository.","Resolve perpetual drift by re-adding the source at its canonical path once.","Clean up stale autopilot lockfiles/signal files before sync.","Run sync from a stable absolute path to avoid drift on CI runners."],"tags":["gbrain","destructive-op-guard","autopilot","drift","safety"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}