unslothai/unsloth · error · RuntimeError

Linked folder is no longer a regular directory

Error message

Linked folder is no longer a regular directory

What it means

RuntimeError raised by _root_identity on a registered folder when lstat succeeds but the path is now a symlink or no longer a directory (e.g. someone replaced the folder with a symlink after registration). This is a TOCTOU defense: identity is keyed on (st_dev, st_ino) of a real directory, and a swapped-in symlink would make the recorded identity point somewhere else, so the sync refuses to proceed. Unlike initial validation (ValueError), this fires on persisted links during sync, indicating the folder changed out from under the registry.

Source

Thrown at studio/backend/core/rag/folder_sync.py:171

    try:
        if Path(normalized) == Path.home().resolve():
            raise ValueError("The entire home folder cannot be registered")
    except RuntimeError:
        pass
    if contains_sensitive_path_component(normalized):
        raise ValueError("Credential or configuration directories are not allowed")
    if is_denied_system_path(normalized):
        raise ValueError("System directories are not allowed")
    return normalized


def _root_identity(root: str) -> tuple[int, int]:
    try:
        root_stat = os.lstat(root)
    except OSError as exc:
        raise RuntimeError("Linked folder is unavailable") from exc
    if stat.S_ISLNK(root_stat.st_mode) or not stat.S_ISDIR(root_stat.st_mode):
        raise RuntimeError("Linked folder is no longer a regular directory")
    if os.path.normcase(os.path.realpath(root)) != os.path.normcase(root):
        raise RuntimeError("Linked folder no longer resolves to its registered path")
    return root_stat.st_dev, root_stat.st_ino


def _store_identity(identity: tuple[int, int]) -> tuple[int | str, int | str]:
    return tuple(value if value <= _SQLITE_INTEGER_MAX else f"x{value:x}" for value in identity)


def _file_identity(metadata: dict) -> tuple[int | str, int | str]:
    return _store_identity((metadata["device"], metadata["inode"]))


def _load_identity(device_id: int | str, file_id: int | str) -> tuple[int, int]:
    def load(value: int | str) -> int:
        return (
            int(value[1:], 16) if isinstance(value, str) and value.startswith("x") else int(value)
        )

View on GitHub (pinned to 203007d190)

Solutions

  1. Remove the symlink and restore a real directory at the registered path, then re-sync.
  2. If the location genuinely moved, unregister the old link and register the new path through validate_folder_path (which accepts the real directory).
  3. Catch this RuntimeError in sync loops and quarantine the offending link rather than aborting all syncs.

Example fix

# before
rm -rf /data/docs && ln -s /mnt/big/docs /data/docs  # sync now fails

# after
rm /data/docs                      # remove the symlink
unregister_folder('/data/docs')    # drop stale record
register_folder('/mnt/big/docs')   # register the real directory
Defensive patterns

Strategy: try-catch

Validate before calling

import os, stat

def folder_still_real_directory(root: str) -> bool:
    try:
        st = os.lstat(root)
    except OSError:
        return False
    return not stat.S_ISLNK(st.st_mode) and stat.S_ISDIR(st.st_mode)

Try / catch

try:
    sync(link)
except RuntimeError as e:
    if "no longer a regular directory" not in str(e):
        raise
    logger.warning("%s replaced with non-directory; re-register the real path", link.path)
    unregister(link)
    notify_user_reregister(link.path)

Prevention

When it happens

Trigger: Deleting the registered folder and recreating the same name as a symlink (or as a file); a mount tool replacing the directory with a symlink to a cache; macOS Finder alias creation over the original folder.

Common situations: Users 'reorganizing' by symlinking the old path to a new location; dev environments where a script recreates directories as links; automation that swaps folder contents via symlink indirection.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/42cc52a3eec0f0f5. Report an issue: GitHub.