odysseus-dev/odysseus · error · HTTPException

Update failed

Error message

Update failed

What it means

500 from POST /{skill_id}/markdown: the parse succeeded and updates were built, but skills_manager.update_skill returned falsy — the manager layer could not persist the new metadata/SKILL.md. Because the skill match was verified moments earlier (830 would have fired), this signals a persistence-layer failure between load and write, typically a race (skill renamed/deleted concurrently) or an I/O error swallowed into a False return.

Source

Thrown at routes/skills_routes.py:1611

            "version": sk.version,
            "category": sk.category,
            "tags": sk.tags,
            "platforms": sk.platforms,
            "requires_toolsets": sk.requires_toolsets,
            "fallback_for_toolsets": sk.fallback_for_toolsets,
            "status": sk.status,
            "confidence": sk.confidence,
            "source": sk.source,
            "teacher_model": sk.teacher_model,
            "owner": sk.owner,
            "when_to_use": sk.when_to_use,
            "procedure": sk.procedure,
            "pitfalls": sk.pitfalls,
            "verification": sk.verification,
            "body_extra": sk.body_extra,
        }, owner=user)
        if not ok:
            raise HTTPException(500, "Update failed")
        # Manual markdown edits can create or substantially rewrite a draft
        # skill without going through /add. Treat unaudited saves as new audit
        # candidates so the event-driven Skills Audit pipeline still runs.
        if not match.get("audit_verdict"):
            _fire_skill_added(user)
        return {"ok": True, "name": sk.name}

    @router.put("/{skill_id}")
    async def update_skill(request: Request, skill_id: str, body: SkillUpdateRequest):
        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)

        updates = body.dict(exclude_none=True)
        if not updates:

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Retry once after reloading the skill list — a transient race is the most common cause.
  2. Check server logs and filesystem permissions/free space on the skills directory.
  3. If consistently failing, GET the skill and confirm its current name matches what the save targets; re-resolve and retry.
  4. Persistent failure with a healthy filesystem is a manager-layer bug — capture logs and report.

Example fix

# before
r = client.post(f"/api/skills/{sid}/markdown", json={"markdown": md})
r.raise_for_status()  # 500 Update failed

# after
for attempt in range(2):
    r = client.post(f"/api/skills/{sid}/markdown", json={"markdown": md})
    if r.status_code != 500:
        break
    skills = client.get("/api/skills").json()  # re-resolve after race
    sid = next(s["name"] for s in skills if s.get("id") == sid or s["name"] == sid)
r.raise_for_status()
Defensive patterns

Strategy: retry

Try / catch

for attempt in range(2):
    r = client.post(f"{base}/api/skills/{sid}/markdown", json={"markdown": md})
    if r.status_code != 500:
        break
    skills = client.get(f"{base}/api/skills").json()  # re-resolve after race
    sid = next((s["name"] for s in skills if s.get("id") == sid or s["name"] == sid), sid)
r.raise_for_status()

Prevention

When it happens

Trigger: Two clients saving the same skill at once and one rename moving the directory between load() and update_skill(); the skills index file or skill directory becoming unwritable mid-request; update_skill refusing because the stored name changed underneath (the pinned-name contract in this route makes stale-name updates fail rather than create).

Common situations: Concurrent edits from two tabs; skills directory on a flaky mount; audit pipeline mutating skills while a manual save is in flight.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/d5ae8b81beddc983. Report an issue: GitHub.