bytedance/deer-flow · error · HTTPException

Skill file not found: {skill_file_path}

Error message

Skill file not found: {skill_file_path}

What it means

HTTP 404 from _load_skill_archive_member (worker thread of GET artifact): the .skill file itself does not exist at the resolved path (actual_skill_path.exists() is False). The archive-level check runs before any member lookup — the whole pack is gone, distinct from a missing internal member.

Source

Thrown at backend/app/gateway/routers/artifacts.py:277

                    return _read_skill_archive_member(zip_ref, info)

            # Not found
            return None
    except (zipfile.BadZipFile, KeyError):
        return None


def _load_skill_archive_member(actual_skill_path: Path, skill_file_path: str, internal_path: str) -> tuple[bytes, str | None]:
    """Worker-thread body for the ``.skill`` branch of ``get_artifact``.

    The ``exists`` / ``is_file`` probes, the ZIP open+extract, and the MIME
    sniff (``mimetypes`` lazily stats the system MIME database on first use) are
    blocking filesystem IO and must stay off the event loop. Raised
    ``HTTPException``s propagate through ``asyncio.to_thread`` unchanged,
    preserving status codes.
    """
    if not actual_skill_path.exists():
        raise HTTPException(status_code=404, detail=f"Skill file not found: {skill_file_path}")
    if not actual_skill_path.is_file():
        raise HTTPException(status_code=400, detail=f"Path is not a file: {skill_file_path}")
    content = _extract_file_from_skill_archive(actual_skill_path, internal_path)
    if content is None:
        raise HTTPException(status_code=404, detail=f"File '{internal_path}' not found in skill archive")
    mime_type, _ = mimetypes.guess_type(internal_path)
    return content, mime_type


def _read_artifact_payload(actual_path: Path, path: str, download: bool) -> tuple[str, str | None]:
    """Worker-thread body for the regular branch of ``get_artifact``.

    Stat probes and MIME sniffing (``mimetypes`` lazily stats the system MIME
    database on first use) are blocking filesystem IO. Returns a
    ``(kind, mime_type)`` plan the handler turns into a streamed
    ``FileResponse``. Inline text and binary previews both use FileResponse so
    clients can request a bounded byte range instead of buffering a whole file.
    """

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Re-list the thread's artifacts to see whether the .skill file still exists under its actual name
  2. If the pack should exist, re-run the step that produces/installs it into outputs
  3. Handle 404 by clearing the preview rather than retrying
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def skill_archive_exists(actual_skill_path: str) -> bool:
    return Path(actual_skill_path).exists()

Try / catch

if resp.status_code == 404 and "Skill file not found" in resp.text:
    clear_preview(); refresh_artifact_list()

Prevention

When it happens

Trigger: GET /api/threads/{thread_id}/artifacts for a .skill path (e.g. 'mnt/user-data/outputs/x.skill#SKILL.md') where the .skill file was deleted or never written to outputs.

Common situations: Previewing a skill pack referenced from stale thread state after the run regenerated outputs, deleted packs, or paths built from old listings.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/6cf81123e5a5a233. Report an issue: GitHub.