sapix technical notes
← all notes

Sep 23, 2026

The cleanup removed the last copy

Cleaning up the skill index, the assistant listed eight rows whose files were gone and deleted them. Five had replacements. Three did not, and two of those were skills I still use, which had been served for six weeks by rows that should not have existed.

After the duplicate rows in the skill index were gone, the assistant ran the cleanup that removes rows whose source file no longer exists. It listed them first, which is the rule for anything that deletes: eight rows, by name. Five had an obvious successor, the same skill now living somewhere else. Three had none. Their files were simply gone, and gone looked like uninstalled, so all eight were deleted.

That is what you would expect an orphan to be: nothing points at it, its source has disappeared, so it is garbage.

A few minutes later a test failed. It was one the assistant had just repaired. It checks that every skill the fallback map routes to actually exists, and it had been skipping in silence since the index fix, because it chose which skills to check by a naming prefix that the fix had changed. Repaired, it reported that the map routed to two skills that no longer existed. They were two of the three with no successor.

They had been moved off disk on purpose, six weeks earlier, by the installer for one of the clients. That client loads every skill it finds at startup, which defeats the point of loading skills only when they are needed, so the installer moves a skill out of its startup folder, but only if the index already serves it. It checked that by name, and the index had the name. What it did not check was where the index read that skill from: through a link that pointed into the very folder the installer was about to empty. The move broke the source of the row that had proved the move was safe.

For six weeks the rows stayed, orphaned, and kept serving both skills by accident. Then the cleanup did exactly its job and removed the last copy of each.

I think there are two lessons here and they are really one. A check keyed on a stand-in for the property you mean fails silently when your own action changes the stand-in. The installer meant “this skill will still be reachable after I move it” and asked “does the index know its name”. The test meant “what is installed” and asked “which rows carry this prefix”. Both questions had correct answers that stopped meaning anything the moment the thing being checked moved.

The other half I have turned into a rule: an orphan with no successor is a question, not a verdict. The five with successors were safe to remove. The three without each needed a reason before anything was deleted, and for two of them the reason was that something had moved them and something else still wanted them.

Both skills are back, served from where they actually live. The installer now asks where the index reads a skill from before it moves anything, and the cleanup I would like next is one that reports an orphan with no successor instead of removing it.