odysseus-dev/odysseus · error · HTTPException
Skill source unavailable
Error message
Skill source unavailable
What it means
404 raised when skills_manager.read_skill_md(name, owner) returns None: the skill is in the invocable index but its SKILL.md file could not be read from disk. The route has already confirmed the skill exists (821 passed), so this is specifically a missing-or-unreadable source file, not a missing skill.
Source
Thrown at routes/skills_routes.py:1343
user = _owner(request)
try:
body = await request.json()
except Exception:
body = {}
request_text = (body.get("request") or "").strip() if isinstance(body, dict) else ""
invokable = {
s.get("name"): s for s in skills_manager.index_for(owner=user)
if (s.get("name") or "").strip()
}
match = invokable.get(skill_id)
if not match:
raise HTTPException(404, "Skill is not available for slash invocation")
name = match.get("name")
md = skills_manager.read_skill_md(name, owner=user)
if md is None:
raise HTTPException(404, "Skill source unavailable")
skills_manager.record_use(name, owner=user)
message = (
"Apply the skill below to my request, following its Procedure / Pitfalls / Verification.\n\n"
f"--- BEGIN SKILL ---\n{md}\n--- END SKILL ---\n\n"
+ (f"Request: {request_text}" if request_text else "Request: (use the skill as appropriate)")
)
return {
"ok": True,
"type": "skill",
"name": name,
"command": f"/{name}",
"message": message,
}
@router.get("/{skill_id}")
async def get_skill(request: Request, skill_id: str):
user = _owner(request)View on GitHub (pinned to f9235ebbf1)
Solutions
- Check the skill's directory on disk: does SKILL.md exist and is it readable by the server process?
- If the file is genuinely gone, re-import the skill or re-create it via POST /add, then re-save metadata.
- If files exist but under a different directory name (rename/orphan case), restore the directory to match the stored skill name or fix the entry to point at the real name.
- Restart the service if the index is cached in memory so it re-scans disk state.
Example fix
# before
resp = client.post(f"/api/skills/{name}/invoke", json={})
assert resp.status_code == 200
# after
resp = client.post(f"/api/skills/{name}/invoke", json={})
if resp.status_code == 404 and "source unavailable" in resp.text:
# index entry exists but SKILL.md is missing on disk — re-import or re-add
skill_md = rebuild_or_fetch_skill_md(name)
client.post(f"/api/skills/{name}/markdown", json={"markdown": skill_md})
resp = client.post(f"/api/skills/{name}/invoke", json={}) Defensive patterns
Strategy: validation
Validate before calling
# probe source availability via the markdown route before invoking
r = client.get(f"{base}/api/skills/{name}/markdown")
if r.status_code == 404:
raise FileNotFoundError(f"SKILL.md missing for '{name}' — index entry is orphaned") Type guard
def has_source(name: str, skills_manager, owner: str) -> bool:
"""Skill is invokable only if its markdown source is readable."""
return skills_manager.read_skill_md(name, owner=owner) is not None Prevention
- Never delete SKILL.md by hand; use the DELETE route so index and disk stay consistent.
- After renames, verify the directory name still matches the stored skill name.
- Monitor for index entries whose files are missing (reconciliation job).
When it happens
Trigger: POST /{skill_id}/invoke for a skill whose index entry survived but whose SKILL.md was deleted, moved, or left unreadable on disk — e.g. the directory was partially deleted, a save was interrupted, the file lives under a renamed directory (the orphaned-id rename bug shape referenced by #1333), or file permissions changed after the index was built.
Common situations: Skills directory edited by hand or by another tool while the server was running; a crashed import/save leaving an index entry without its markdown; skills migrated between machines with an index/cache copied but files skipped; containerized deployments where the volume mount dropped files.
Related errors
- Skill is not available for slash invocation
- HTTP ${res.status}
- Skill import failed
- Skill source unavailable (legacy entry?)
- Stream closed before completion
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/e0569c077a075976.
Report an issue: GitHub.