unslothai/unsloth · error · FileNotFoundError

lockfile not found: {path}

Error message

lockfile not found: {path}

What it means

ResearchApiError thrown by the shared json() helper in research-api.ts:49-65 when a /api/chat/research-runs request fails and the error body contains neither a string `detail` nor a string `message`. Unlike a plain Error, it carries the HTTP status on error.status so callers (e.g. isPermanentResearchError at research-api.ts:216) can distinguish retryable 5xx from permanent 4xx.

Source

Thrown at scripts/check_new_install_scripts.py:119

            scripts = entry.get("scripts") or {}
            lifecycle = any(
                isinstance(scripts, dict) and scripts.get(hook)
                for hook in ("preinstall", "install", "postinstall")
            )
            if lifecycle:
                ver = entry.get("version") or "<unversioned>"
                seen[f"{name}@{ver}"] = name
            _walk_v1(entry.get("dependencies"), depth = depth + 1)

    if version == 1 or "dependencies" in lock:
        _walk_v1(lock.get("dependencies") or {})

    return seen


def _load_lockfile(path: Path) -> dict:
    if not path.exists():
        raise FileNotFoundError(f"lockfile not found: {path}")
    try:
        return json.loads(path.read_text(encoding = "utf-8"))
    except json.JSONDecodeError as exc:
        raise ValueError(f"{path}: not valid JSON: {exc}") from exc


# Registry lookup for the postinstall command body (best-effort).


def _fetch_registry_scripts(name: str, version: str) -> dict[str, str] | None:
    """Return {hook: command} for lifecycle hooks in registry metadata; None on any error (never raises)."""
    safe_name = urllib.parse.quote(name, safe = "@/")
    url = f"{REGISTRY_BASE}{safe_name}/{urllib.parse.quote(version)}"
    try:
        with urllib.request.urlopen(url, timeout = REGISTRY_TIMEOUT_SECS) as resp:
            body = resp.read()
    except (urllib.error.URLError, OSError, ValueError, TimeoutError):
        return None

View on GitHub (pinned to 203007d190)

Solutions

  1. Inspect error.status in a catch: if >= 500 treat as transient and retry via the existing reconnect logic; if 4xx fix the request payload.
  2. Reproduce the failing request in the Network tab and read the raw response body to find the server-side cause.
  3. If auth-related (401), re-login and retry.
  4. As a developer, stringify non-string detail values (FastAPI validation arrays) before falling back to the generic message.

Example fix

// before
const detail = (body as { detail?: unknown })?.detail;

// after — stringify FastAPI structured validation details
const detail = body?.detail;
const detailText = typeof detail === "string" ? detail : detail ? JSON.stringify(detail) : undefined;
Defensive patterns

Strategy: retry

Type guard

function isResearchApiError(e: unknown): e is ResearchApiError {
  return e instanceof Error && e.name === "ResearchApiError" && typeof (e as ResearchApiError).status === "number";
}

Try / catch

try {
  await createResearchRun(input);
} catch (error) {
  if (isResearchApiError(error)) {
    if (error.status >= 500) { /* transient: backoff and retry */ }
    else { /* permanent 4xx: fix payload, do not retry */ }
  } else throw error;
}

Prevention

When it happens

Trigger: POST /api/chat/research-runs, or any mutate() call, or the streamResearchEvents !response.ok path returning an error whose body is empty, null (json parse failed), or has non-string detail — e.g. a 500 with an empty body, a 422 with a structured (array/object) validation detail, or a proxied HTML error page.

Common situations: Backend research worker crashed mid-run (500 with no detail); misconfigured reverse proxy returning 502 HTML; FastAPI validation errors where detail is a list of objects rather than a string; expired auth yielding a non-JSON 401.

Related errors


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