A migration refuses to run and reports that it needs two distinct databases, but the two paths it names are spelled differently and both exist. On macOS the culprit is often a single-letter case difference - an agent directory created as BA-EA while the configured identifier is ba-ea. This is openclaw/openclaw#174089, fixed in PR #174243.
Why it happens
macOS and Windows default to case-insensitive filesystems. Two path strings that differ only in letter case resolve to the same inode, so agents/BA-EA/state.sqlite and agents/ba-ea/state.sqlite are one physical file. A guard that decides whether a source and a destination are distinct by comparing the raw path strings sees two different strings and concludes they are separate. The copy routine underneath, which correctly recognises the physical identity, then refuses because source and destination are the same database.
The result is a repair that stops halfway - the higher-level step blocks every later migration, and nothing self-heals because both spellings keep resolving to the same file.
The fix
Compare filesystem identity rather than path strings when deciding whether two stores are the same. Where you already have a physical-identity matcher, route selection, grouping and cleanup through it. If a copy genuinely targets the same physical database, treat it as a no-op instead of an error.
The concrete failure is the string-based branch:
// fragile - two spellings of one file look distinct
if (sourcePath === destinationPath) throw new Error(
'copy requires distinct source and destination databases'
)
Recognise the alias before the check runs:
const sameFile = fs.realpathSync(sourcePath) === fs.realpathSync(destinationPath)
if (sameFile) return // no-op - one physical database
The wider lesson is worth keeping for any tool that keys state on identifiers derived from names. If a name can differ in case but the state lives on a case-insensitive disk, canonicalise the key before it becomes a path. Two IDs that are distinct in the data model can be one file on disk, and a guard written against the data model will keep tripping over the disk.