odysseus-dev/odysseus · error · Error

data.detail || 'Auto-sort failed'

Error message

data.detail || 'Auto-sort failed'

What it means

Raised by _fetch_text when the bytes fetched by _fetch_bytes cannot be decoded as UTF-8. The importer only handles text files (every bundle entry maps relative path to decoded str), so a binary asset — image, PDF, zip, sqlite db — is rejected at decode time.

Source

Thrown at static/app.js:1427

    _syncSortChecks();

    // AI auto-sort — spinner on the sort button itself. Used by both
    // the main "★ Tidy" button (AI) and the sub-row "Tidy" button
    // (no AI, Phase 1 cleanup only) via the skipLlm flag.
    async function _runTidy(skipLlm) {
      const btnIcon = sortBtn.querySelector('.sort-icon');
      if (btnIcon) btnIcon.style.display = 'none';
      const wp = spinnerModule.create('', 'clean', 'whirlpool');
      const wpEl = wp.createElement();
      wpEl.style.cssText = 'width:13px;height:13px;display:inline-block;vertical-align:middle;margin-top:-5px;';
      sortBtn.appendChild(wpEl);
      wp.start();
      sortDropdown.style.display = 'none';
      try {
        const url = `${API_BASE}/api/sessions/auto-sort${skipLlm ? '?skip_llm=true' : ''}`;
        const res = await fetch(url, { method: 'POST' });
        const data = await res.json();
        if (!res.ok) throw new Error(data.detail || 'Auto-sort failed');
        if (data.status === 'ok') {
          sessionModule.setSortMode(null); // clear sort — tidy creates manual folder order
          _syncSortChecks();
          if (skipLlm) {
            // No-AI path: just report what got cleaned. No "unfiled
            // remaining" prompt because we never tried to file anything.
            const cleaned = (data.deleted_empty || 0) + (data.deleted_throwaway || 0);
            uiModule.showToast(cleaned ? `Cleaned ${cleaned} empty/throwaway chat${cleaned === 1 ? '' : 's'}` : 'Already clean');
          } else {
            // Tidy now works in batches (15 most-recent unfiled per click)
            // so the user gets fast feedback and a manageable LLM call
            // even with hundreds of chats. Tell them what's left.
            const remaining = data.unfiled_remaining || 0;
            let msg;
            if (data.updated > 0) {
              msg = `Sorted ${data.updated} into ${data.folders.length} folder${data.folders.length === 1 ? '' : 's'}`;
              if (remaining > 0) msg += ` — ${remaining} unfiled left, hit Group again`;
            } else if (remaining > 0) {

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Remove binary assets from the skill folder — reference images/docs by absolute URL inside SKILL.md instead
  2. If the file is meant to be text, re-save it as UTF-8 (watch for UTF-16 exports from Windows editors or BOM issues)
  3. Rename mislabeled files so the real extension matches the real content, keeping binary extensions out of the bundle
  4. Ensure you linked a SKILL.md file or a folder containing SKILL.md, not an asset file

Example fix

# before
my-skill/
  SKILL.md
  architecture.png   # binary -> 'non-text file' during traversal

# after
my-skill/
  SKILL.md           # ![architecture](https://raw.githubusercontent.com/.../architecture.png)
Defensive patterns

Strategy: validation

Validate before calling

_TEXT_EXT = {".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".py", ".js", ".ts", ".sh", ""}

def is_probably_text_asset(name: str) -> bool:
    return Path(name).suffix.lower() in _TEXT_EXT

def decodes_as_utf8(data: bytes) -> bool:
    try:
        data.decode("utf-8")
        return True
    except UnicodeDecodeError:
        return False

Try / catch

try:
    files, src = fetch_skill_bundle(url)
except SkillImportError as e:
    if "non-text file" in str(e):
        report(f"{url}: binary asset in bundle — remove it or reference it by URL in SKILL.md")
        return None
    raise

Prevention

When it happens

Trigger: A direct /blob/... URL pointing at a binary file (e.g. logo.png), or a directory traversal where _is_text_file(name) matched the extension but the content is binary (a .md/.txt file that is actually binary, or a mislabeled extension). _fetch_bytes succeeds, then data.decode('utf-8') raises UnicodeDecodeError which is chained into this SkillImportError.

Common situations: Skill authors commit screenshots, PDFs, or fonts alongside SKILL.md; users link directly to a binary in the repo. Also a text file saved in UTF-16/Latin-1 with a .md extension fails strict UTF-8 decoding.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/d2e036f8d1577584. Report an issue: GitHub.