{"record":{"id":"d5ae8b81beddc983","repo":"odysseus-dev/odysseus","slug":"update-failed","errorCode":null,"errorMessage":"Update failed","messagePattern":"Update failed","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/skills_routes.py","lineNumber":1611,"sourceCode":"            \"version\": sk.version,\n            \"category\": sk.category,\n            \"tags\": sk.tags,\n            \"platforms\": sk.platforms,\n            \"requires_toolsets\": sk.requires_toolsets,\n            \"fallback_for_toolsets\": sk.fallback_for_toolsets,\n            \"status\": sk.status,\n            \"confidence\": sk.confidence,\n            \"source\": sk.source,\n            \"teacher_model\": sk.teacher_model,\n            \"owner\": sk.owner,\n            \"when_to_use\": sk.when_to_use,\n            \"procedure\": sk.procedure,\n            \"pitfalls\": sk.pitfalls,\n            \"verification\": sk.verification,\n            \"body_extra\": sk.body_extra,\n        }, owner=user)\n        if not ok:\n            raise HTTPException(500, \"Update failed\")\n        # Manual markdown edits can create or substantially rewrite a draft\n        # skill without going through /add. Treat unaudited saves as new audit\n        # candidates so the event-driven Skills Audit pipeline still runs.\n        if not match.get(\"audit_verdict\"):\n            _fire_skill_added(user)\n        return {\"ok\": True, \"name\": sk.name}\n\n    @router.put(\"/{skill_id}\")\n    async def update_skill(request: Request, skill_id: str, body: SkillUpdateRequest):\n        user = _owner(request)\n        skills = skills_manager.load(owner=user)\n        match = next((s for s in skills if s.get(\"name\") == skill_id or s.get(\"id\") == skill_id), None)\n        if not match:\n            raise HTTPException(404, \"Skill not found\")\n        _verify_owner(match, user)\n\n        updates = body.dict(exclude_none=True)\n        if not updates:","sourceCodeStart":1593,"sourceCodeEnd":1629,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1593-L1629","documentation":"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.","triggerScenarios":"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).","commonSituations":"Concurrent edits from two tabs; skills directory on a flaky mount; audit pipeline mutating skills while a manual save is in flight.","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."],"exampleFix":"# before\nr = client.post(f\"/api/skills/{sid}/markdown\", json={\"markdown\": md})\nr.raise_for_status()  # 500 Update failed\n\n# after\nfor attempt in range(2):\n    r = client.post(f\"/api/skills/{sid}/markdown\", json={\"markdown\": md})\n    if r.status_code != 500:\n        break\n    skills = client.get(\"/api/skills\").json()  # re-resolve after race\n    sid = next(s[\"name\"] for s in skills if s.get(\"id\") == sid or s[\"name\"] == sid)\nr.raise_for_status()","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for attempt in range(2):\n    r = client.post(f\"{base}/api/skills/{sid}/markdown\", json={\"markdown\": md})\n    if r.status_code != 500:\n        break\n    skills = client.get(f\"{base}/api/skills\").json()  # re-resolve after race\n    sid = next((s[\"name\"] for s in skills if s.get(\"id\") == sid or s[\"name\"] == sid), sid)\nr.raise_for_status()","preventionTips":["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."],"tags":["skills","http-500","persistence","race-condition","concurrency"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}