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
- Retry once after reloading the skill list — a transient race is the most common cause.
- Check server logs and filesystem permissions/free space on the skills directory.
- If consistently failing, GET the skill and confirm its current name matches what the save targets; re-resolve and retry.
- 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
- Keep the edited text client-side so a failed save never loses work.
- Retry once after re-resolving the skill name — most failures are load/write races.
- Monitor skills-directory permissions and disk space.
- Avoid concurrent saves of the same skill from multiple tabs.
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
- Failed to list calendars
- Failed to update document: {e}
- Skill import failed
- d.error || 'Failed to create calendar'
- Failed to load SKILL.md
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/d5ae8b81beddc983.
Report an issue: GitHub.