odysseus-dev/odysseus · warning · Error
Failed to load SKILL.md
Error message
Failed to load SKILL.md
What it means
Raised inside _fetchSkillMarkdown (static/js/skills.js:40) when GET /api/skills/{name}/markdown returns non-2xx; the caller catches it and shows 'Failed to load SKILL.md'. The function caches markdown by skill name in _mdCache, so once one request fails the failure is not cached — every expand retries the fetch.
Source
Thrown at static/js/skills.js:40
let _cascadeNext = false; // set true to play the domino-in entrance on the next render
function _playSkillsCascade(container = document.getElementById('skills-list')) {
if (!container || !container.querySelector('.skill-card')) return false;
container.classList.remove('doclib-just-opened');
void container.offsetWidth;
container.classList.add('doclib-just-opened');
setTimeout(() => container.classList.remove('doclib-just-opened'), 900);
return true;
}
// Cache of SKILL.md text by skill name, so expanding is instant (no async
// fetch + content-settle jump). Populated lazily on expand AND eagerly in
// the background for all visible cards right after render.
const _mdCache = new Map();
async function _fetchSkillMarkdown(name) {
if (_mdCache.has(name)) return _mdCache.get(name);
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const md = data.markdown || '';
_mdCache.set(name, md);
return md;
}
// Background-load the markdown for every currently-rendered skill card so it
// is ready (in the card's <pre> + _mdLoaded) before the user expands it.
function _preloadVisibleMarkdown() {
document.querySelectorAll('#skills-list .skill-card[data-skill-name]').forEach(card => {
const name = card.dataset.skillName;
if (!name || card._mdLoaded) return;
const pre = card.querySelector('.skill-md-pre');
const apply = (md) => { if (pre) pre.textContent = md || '(empty)'; card._mdLoaded = true; card._md = md || ''; };
if (_mdCache.has(name)) { apply(_mdCache.get(name)); return; }
_fetchSkillMarkdown(name).then(apply).catch(() => {});
});
}
View on GitHub (pinned to f9235ebbf1)
Solutions
- Check GET /api/skills/{name}/markdown status: 404 → the SKILL.md is gone, refresh the skill list (loadSkills) to reconcile; 401 → re-login; 500 → server log.
- Confirm the skill name is exactly the directory name (encodeURIComponent handles spaces, but not route-parameter slashes).
- Retry the expand once after refreshing — the failed attempt is not cached, so a refresh-and-expand recovers.
- If it persists, verify the skills directory path in server config exists and is readable by the server process.
Example fix
// before
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// after — cache 404s negatively so preloads stop hammering a dead skill
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
if (!res.ok) {
if (res.status === 404) _mdCache.set(name, null); // negative cache
throw new Error(`HTTP ${res.status}`);
} Defensive patterns
Strategy: fallback
Validate before calling
if (_mdCache.has(name)) { const cached = _mdCache.get(name); if (cached === null) return; /* known-missing */ } Try / catch
try { const md = await _fetchSkillMarkdown(name); } catch (e) { /* show inline error with e.message (HTTP N); do NOT cache the failure so a retry after refresh works */ } Prevention
- Negative-cache 404s so background preloading stops re-fetching dead skills.
- Refresh the skills list (loadSkills) before retrying — most 404s are stale list entries.
- Include the HTTP status in the user-facing message.
- Verify skill names match on-disk directories exactly.
When it happens
Trigger: Expanding a skill card (or the background _preloadVisibleMarkdown sweep) requesting the skill's markdown. 404 when the skill directory/SKILL.md does not exist (deleted on disk, draft never materialized, name mismatch); 401 when the session expired; 500 when the file read fails server-side.
Common situations: Skill folder renamed or deleted on disk while the list still shows it; URL-encoded skill names with slashes or special characters mismatching the route; skills directory mounted read-only or missing in a container; stale page after server-side skill changes.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/28233c410d574133.
Report an issue: GitHub.