odysseus-dev/odysseus · warning · HTTPException
Skill source unavailable (legacy entry?)
Error message
Skill source unavailable (legacy entry?)
What it means
404 from GET /{skill_id}/markdown raised when the skill WAS found and ownership verified, but skills_manager.read_skill_md returned None — the index entry exists without a readable SKILL.md behind it. The '(legacy entry?)' hint marks entries created before skills stored a SKILL.md file, which carry only structured metadata and no raw source.
Source
Thrown at routes/skills_routes.py:1380
skills = skills_manager.load(owner=user)
for sk in skills:
if sk.get("name") == skill_id or sk.get("id") == skill_id:
return sk
raise HTTPException(404, "Skill not found")
@router.get("/{skill_id}/markdown")
async def get_skill_markdown(request: Request, skill_id: str):
"""Return the raw SKILL.md text — used by the slash-invocation flow
and the editor's 'view source' affordance."""
user = _owner(request)
skills = skills_manager.load(owner=user)
match = next((s for s in skills if s.get("name") == skill_id or s.get("id") == skill_id), None)
if not match:
raise HTTPException(404, "Skill not found")
_verify_owner(match, user)
md = skills_manager.read_skill_md(match.get("name"), owner=user)
if md is None:
raise HTTPException(404, "Skill source unavailable (legacy entry?)")
return {"name": match.get("name"), "markdown": md}
@router.post("/{skill_id}/test")
async def test_skill(request: Request, skill_id: str):
"""Kick off a background skill test (agent run + LLM judge). Returns
immediately; the run executes server-side so it survives the modal being
closed. Poll GET /{skill_id}/test-status for progress + verdict.
On completion it records the verdict and nudges the skill's confidence
to match (pass→0.95, needs_work→0.6, fail→0.4; inconclusive/unknown leave
it untouched). It never changes the skill's published/draft STATUS."""
import time as _time
import asyncio as _asyncio
from src.endpoint_resolver import resolve_endpoint
user = _owner(request)
body = await request.json()
task = (body.get("task") or "").strip()
View on GitHub (pinned to f9235ebbf1)
Solutions
- Check the skill directory on disk for SKILL.md; if absent, the entry is legacy.
- Regenerate the source: reconstruct a SKILL.md from the entry's structured fields (description, procedure, pitfalls, verification) and POST it to /{skill_id}/markdown to backfill.
- If the file exists but is unreadable, fix permissions and retry.
- Alternatively re-create the skill via POST /add so it is stored in the current format.
Example fix
# before
md = client.get(f"/api/skills/{sid}/markdown").json()["markdown"]
# after
r = client.get(f"/api/skills/{sid}/markdown")
if r.status_code == 404 and "legacy" in r.text:
meta = client.get(f"/api/skills/{sid}").json()
reconstructed = f"---\nname: {meta['name']}\ndescription: {meta['description']}\n---\n"
client.post(f"/api/skills/{sid}/markdown", json={"markdown": reconstructed})
r = client.get(f"/api/skills/{sid}/markdown")
md = r.json()["markdown"] Defensive patterns
Strategy: fallback
Validate before calling
meta = client.get(f"{base}/api/skills/{sid}").json()
# legacy entries carry structured fields but no markdown source
needs_backfill = "markdown" not in meta and not skill_dir_has_skill_md(meta["name"]) Type guard
def is_legacy_entry(skill: dict) -> bool:
"""Heuristic: entry predates markdown storage when source is unset and status fields exist."""
return not skill.get("source") and bool(skill.get("description")) Try / catch
try:
md = client.get(f"{base}/api/skills/{sid}/markdown").json()["markdown"]
except KeyError if client_last_status == 404 else None:
pass
# better: check status explicitly
r = client.get(f"{base}/api/skills/{sid}/markdown")
if r.status_code == 404 and "legacy" in str(r.json().get("detail", "")):
md = render_markdown_from_fields(client.get(f"{base}/api/skills/{sid}").json()) Prevention
- After upgrades, run a migration that materializes SKILL.md for every legacy entry.
- Test 'view source' across the whole skill list right after upgrading.
- Keep structured fields and markdown in sync going forward via the /markdown save route.
When it happens
Trigger: Viewing source of a legacy skill created by an older version that stored fields without a SKILL.md file; a skill whose file was deleted or stranded by a rename while the entry remained; a directory whose SKILL.md lost read permission.
Common situations: Upgrading an installation that predates the markdown-backed skill format; partial migrations where only some skills were converted; hand-edited skills directories.
Related errors
- Skill is not available for slash invocation
- Skill source unavailable
- markdown is required
- Could not parse SKILL.md: {e}
- Stream closed before completion
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/a1119b9e0930a522.
Report an issue: GitHub.