{"record":{"id":"e6c5029366f9645b","repo":"EveryInc/compound-engineering-plugin","slug":"collectionpath-changed-since-it-was-inspected","errorCode":null,"errorMessage":"${collectionPath} changed since it was inspected; refusing to remove it${detail ? `. ${detail}` : \"\"}","messagePattern":"(.+?) changed since it was inspected; refusing to remove it(.+?)` : \"\"\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/dev/codex-dev.ts","lineNumber":293,"sourceCode":"\n  await replaceManagedCollectionLink(context.collectionPath, desiredTarget, state)\n}\n\nexport type ManagedCollectionLinkExpectation =\n  | { kind: \"absent\" }\n  | { kind: \"valid\"; target: string }\n\nexport async function removeManagedCollectionLink(\n  collectionPath: string,\n  expectedTarget: string,\n  options: {\n    ignoreChanges?: boolean\n    onTakenForTest?: (recoveryPath: string) => Promise<void>\n  } = {},\n): Promise<boolean> {\n  const changed = (detail?: string): false => {\n    if (options.ignoreChanges) return false\n    throw new Error(\n      `${collectionPath} changed since it was inspected; refusing to remove it${detail ? `. ${detail}` : \"\"}`,\n    )\n  }\n\n  const parentPath = path.dirname(collectionPath)\n  const recoveryDir = await fs.mkdtemp(\n    path.join(parentPath, `.${path.basename(collectionPath)}.recovery-`),\n  )\n  const recoveryPath = path.join(recoveryDir, \"entry\")\n\n  try {\n    await fs.rename(collectionPath, recoveryPath)\n  } catch (error) {\n    await fs.rmdir(recoveryDir).catch(() => undefined)\n    if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return changed()\n    throw error\n  }\n","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/EveryInc/compound-engineering-plugin/blob/c9c10f8c75412c7232cb2bd663e5fd1cea98d84e/src/dev/codex-dev.ts#L275-L311","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-run the command — a single clean invocation after the race window closes will usually succeed","Ensure no other codex:dev process or file-sync tool is touching CODEX_HOME concurrently","Verify the path's current state with `bun run codex:dev -- status` and retry","Only pass ignoreChanges in controlled test fixtures, never in interactive use"],"exampleFix":"// before — two concurrent invocations racing on the same collection path\nawait Promise.all([switchToRemote(ctx), removeLocalCollection(ctx)])\n\n// after — serialize the operations\nawait switchToRemote(ctx)\nawait removeLocalCollection(ctx)","handlingStrategy":"retry","validationCode":"// ensure the path is stable before removal\nconst before = await inspectLocalCollection(context);\nawait new Promise(r => setTimeout(r, 100));\nconst after = await inspectLocalCollection(context);\nif (JSON.stringify(before) !== JSON.stringify(after)) throw new Error(\"path is being mutated concurrently\");","typeGuard":"function expectsState(kind: string): boolean {\n  return [\"valid\", \"broken\"].includes(kind);\n}","tryCatchPattern":"try {\n  await removeLocalCollection(context);\n} catch (e) {\n  if (String(e).includes(\"changed since it was inspected\")) {\n    await removeLocalCollection(context); // retry once after the race clears\n  } else throw e;\n}","preventionTips":["Do not run overlapping codex:dev commands against the same CODEX_HOME","Pause file-sync tools (Dropbox/iCloud) that watch the skills directory","Retry the command once before investigating — the guard is designed for transient races"],"tags":["race-condition","filesystem","symlink","codex-dev"],"backgroundTag":"path-changed-race-condition","analyzedSha":"c9c10f8c75412c7232cb2bd663e5fd1cea98d84e","analyzedAt":"2026-08-31T15:18:07.959Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}