{"record":{"id":"d2e036f8d1577584","repo":"odysseus-dev/odysseus","slug":"data-detail-auto-sort-failed","errorCode":null,"errorMessage":"data.detail || 'Auto-sort failed'","messagePattern":"data\\.detail \\|\\| 'Auto-sort failed'","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/app.js","lineNumber":1427,"sourceCode":"    _syncSortChecks();\n\n    // AI auto-sort — spinner on the sort button itself. Used by both\n    // the main \"★ Tidy\" button (AI) and the sub-row \"Tidy\" button\n    // (no AI, Phase 1 cleanup only) via the skipLlm flag.\n    async function _runTidy(skipLlm) {\n      const btnIcon = sortBtn.querySelector('.sort-icon');\n      if (btnIcon) btnIcon.style.display = 'none';\n      const wp = spinnerModule.create('', 'clean', 'whirlpool');\n      const wpEl = wp.createElement();\n      wpEl.style.cssText = 'width:13px;height:13px;display:inline-block;vertical-align:middle;margin-top:-5px;';\n      sortBtn.appendChild(wpEl);\n      wp.start();\n      sortDropdown.style.display = 'none';\n      try {\n        const url = `${API_BASE}/api/sessions/auto-sort${skipLlm ? '?skip_llm=true' : ''}`;\n        const res = await fetch(url, { method: 'POST' });\n        const data = await res.json();\n        if (!res.ok) throw new Error(data.detail || 'Auto-sort failed');\n        if (data.status === 'ok') {\n          sessionModule.setSortMode(null); // clear sort — tidy creates manual folder order\n          _syncSortChecks();\n          if (skipLlm) {\n            // No-AI path: just report what got cleaned. No \"unfiled\n            // remaining\" prompt because we never tried to file anything.\n            const cleaned = (data.deleted_empty || 0) + (data.deleted_throwaway || 0);\n            uiModule.showToast(cleaned ? `Cleaned ${cleaned} empty/throwaway chat${cleaned === 1 ? '' : 's'}` : 'Already clean');\n          } else {\n            // Tidy now works in batches (15 most-recent unfiled per click)\n            // so the user gets fast feedback and a manageable LLM call\n            // even with hundreds of chats. Tell them what's left.\n            const remaining = data.unfiled_remaining || 0;\n            let msg;\n            if (data.updated > 0) {\n              msg = `Sorted ${data.updated} into ${data.folders.length} folder${data.folders.length === 1 ? '' : 's'}`;\n              if (remaining > 0) msg += ` — ${remaining} unfiled left, hit Group again`;\n            } else if (remaining > 0) {","sourceCodeStart":1409,"sourceCodeEnd":1445,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/app.js#L1409-L1445","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove binary assets from the skill folder — reference images/docs by absolute URL inside SKILL.md instead","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)","Rename mislabeled files so the real extension matches the real content, keeping binary extensions out of the bundle","Ensure you linked a SKILL.md file or a folder containing SKILL.md, not an asset file"],"exampleFix":"# before\nmy-skill/\n  SKILL.md\n  architecture.png   # binary -> 'non-text file' during traversal\n\n# after\nmy-skill/\n  SKILL.md           # ![architecture](https://raw.githubusercontent.com/.../architecture.png)","handlingStrategy":"validation","validationCode":"_TEXT_EXT = {\".md\", \".txt\", \".json\", \".yaml\", \".yml\", \".toml\", \".py\", \".js\", \".ts\", \".sh\", \"\"}\n\ndef is_probably_text_asset(name: str) -> bool:\n    return Path(name).suffix.lower() in _TEXT_EXT\n\ndef decodes_as_utf8(data: bytes) -> bool:\n    try:\n        data.decode(\"utf-8\")\n        return True\n    except UnicodeDecodeError:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    files, src = fetch_skill_bundle(url)\nexcept SkillImportError as e:\n    if \"non-text file\" in str(e):\n        report(f\"{url}: binary asset in bundle — remove it or reference it by URL in SKILL.md\")\n        return None\n    raise","preventionTips":["Never commit binaries (png/pdf/zip/db) inside a skill folder; reference them by absolute URL","Save text files as UTF-8, not UTF-16/Latin-1","Lint skill repos for binary signatures in *.md/*.txt files"],"tags":["encoding","binary-file","utf-8","skill-import"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}