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

  1. 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.
  2. Confirm the skill name is exactly the directory name (encodeURIComponent handles spaces, but not route-parameter slashes).
  3. Retry the expand once after refreshing — the failed attempt is not cached, so a refresh-and-expand recovers.
  4. 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

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.