EveryInc/compound-engineering-plugin · error · Error

${collectionPath} changed since it was inspected; refusing t

Error message

${collectionPath} changed since it was inspected; refusing to remove it${detail ? `. ${detail}` : ""}

What it means

removeManagedCollectionLink() takes a snapshot ('takes') the entry at collectionPath for validation before deleting it, after checking it still matches the state returned by inspectLocalCollection(). The inner changed() helper aborts the removal if the path mutated between inspection and removal, protecting against destroying an entry the caller never saw or agreed to remove. options.ignoreChanges opts out (used by tests).

Source

Thrown at src/dev/codex-dev.ts:293

  await replaceManagedCollectionLink(context.collectionPath, desiredTarget, state)
}

export type ManagedCollectionLinkExpectation =
  | { kind: "absent" }
  | { kind: "valid"; target: string }

export async function removeManagedCollectionLink(
  collectionPath: string,
  expectedTarget: string,
  options: {
    ignoreChanges?: boolean
    onTakenForTest?: (recoveryPath: string) => Promise<void>
  } = {},
): Promise<boolean> {
  const changed = (detail?: string): false => {
    if (options.ignoreChanges) return false
    throw new Error(
      `${collectionPath} changed since it was inspected; refusing to remove it${detail ? `. ${detail}` : ""}`,
    )
  }

  const parentPath = path.dirname(collectionPath)
  const recoveryDir = await fs.mkdtemp(
    path.join(parentPath, `.${path.basename(collectionPath)}.recovery-`),
  )
  const recoveryPath = path.join(recoveryDir, "entry")

  try {
    await fs.rename(collectionPath, recoveryPath)
  } catch (error) {
    await fs.rmdir(recoveryDir).catch(() => undefined)
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return changed()
    throw error
  }

View on GitHub (pinned to c9c10f8c75)

Solutions

  1. Re-run the command — a single clean invocation after the race window closes will usually succeed
  2. Ensure no other codex:dev process or file-sync tool is touching CODEX_HOME concurrently
  3. Verify the path's current state with `bun run codex:dev -- status` and retry
  4. Only pass ignoreChanges in controlled test fixtures, never in interactive use

Example fix

// before — two concurrent invocations racing on the same collection path
await Promise.all([switchToRemote(ctx), removeLocalCollection(ctx)])

// after — serialize the operations
await switchToRemote(ctx)
await removeLocalCollection(ctx)
Defensive patterns

Strategy: retry

Validate before calling

// ensure the path is stable before removal
const before = await inspectLocalCollection(context);
await new Promise(r => setTimeout(r, 100));
const after = await inspectLocalCollection(context);
if (JSON.stringify(before) !== JSON.stringify(after)) throw new Error("path is being mutated concurrently");

Type guard

function expectsState(kind: string): boolean {
  return ["valid", "broken"].includes(kind);
}

Try / catch

try {
  await removeLocalCollection(context);
} catch (e) {
  if (String(e).includes("changed since it was inspected")) {
    await removeLocalCollection(context); // retry once after the race clears
  } else throw e;
}

Prevention

When it happens

Trigger: The file/symlink at collectionPath is created, replaced, retargeted, or deleted between the inspectLocalCollection() call that produced the expected state and the actual rename performed inside removeManagedCollectionLink(); e.g. a concurrent codex:dev run or another process touched the path in that window.

Common situations: Two `bun run codex:dev` commands racing (one switching to remote while the other removes the local link); a watcher or sync tool (Dropbox, editor auto-save) recreating the skills path mid-operation; the user manually editing the collection path while a command runs.

Related errors


AI-assisted analysis of EveryInc/compound-engineering-plugin@c9c10f8c75 (2026-08-31). Data as JSON: /api/errors/e6c5029366f9645b. Report an issue: GitHub.