{"record":{"id":"eb91e50d195f73df","repo":"zylon-ai/private-gpt","slug":"first-message-in-the-block-must-be-a-user-message","errorCode":null,"errorMessage":"First message in the block must be a USER message.","messagePattern":"First message in the block must be a USER message\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"private_gpt/chat/input_models.py","lineNumber":719,"sourceCode":"\n        for block in user_blocks:\n            if not block:\n                continue\n\n            # Find TLDR messages and their positions in this block\n            tldr_positions: list[int] = []\n            for j, message in enumerate(block):\n                if _has_tldr_content(message):\n                    tldr_positions.append(j)\n\n            if not tldr_positions:\n                # No TLDR in this block, keep all messages as-is\n                add_by_id(block)\n                continue\n\n            user_message: ChatMessage = block[0]\n            if user_message and user_message.role != MessageRole.USER:\n                raise ValueError(\"First message in the block must be a USER message.\")\n\n            # Group consecutive TLDR positions together so we can detect accumulated\n            # TLDRBlocks (where each subsequent block repeats all prior entries plus\n            # new ones at the end).\n            tldr_positions_grouped_by_consecutive = [\n                [num for _, num in group]\n                for _, group in groupby(\n                    enumerate(tldr_positions), lambda x: x[1] - x[0]\n                )\n            ]\n\n            # Tracks how many TLDR ChatMessages were emitted by the previous group.\n            # Accumulated right-side TLDRBlocks re-emit all prior summaries as a\n            # prefix, so we skip that prefix to avoid duplicates while still\n            # preserving distinct instances with identical text (e.g. TOOL(\"-\")).\n            prev_group_tldr_count = 0\n\n            for i, pos in enumerate(tldr_positions_grouped_by_consecutive):","sourceCodeStart":701,"sourceCodeEnd":737,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/chat/input_models.py#L701-L737","documentation":"Raised by DELETE /skills/{skill_id} when the skill exists but has readonly=True. Readonly skills are provisioned (e.g. built-in or admin-managed) and are protected from deletion; the route returns 403 Forbidden before calling delete_skill.","triggerScenarios":"Attempting to delete a builtin/system skill flagged readonly in its collection; a UI that shows readonly skills with an active delete button.","commonSituations":"Trying to remove bundled skills shipped with the deployment; permission model changes that marked existing skills readonly after an upgrade.","solutions":["Do not expose delete actions for skills where the API reports readonly=true.","If removal is genuinely required, ask an operator to remove/flag the skill at the storage/admin level rather than the API.","Check the skill metadata first (GET) and branch on skill.readonly."],"exampleFix":"// before\nawait skillsApi.delete(skill.id, collection);\n\n// after\nif (skill.readonly) { toast('Readonly skill cannot be deleted'); return; }\nawait skillsApi.delete(skill.id, collection);","handlingStrategy":"type-guard","validationCode":"const skill = await skillsApi.get(id, collection);\nif (skill?.readonly) { throw new ReadOnlySkillError(id); }","typeGuard":"const isDeletableSkill = (s) => s != null && !s.readonly;","tryCatchPattern":"try { await skillsApi.delete(id, collection); }\ncatch (e) {\n  if (e.status === 403 && /readonly/.test(e.detail)) { /* hide delete UI */ }\n  else throw e;\n}","preventionTips":["Read the readonly flag from skill metadata before enabling destructive actions","Filter readonly skills out of bulk operations","For readonly content you must change, clone it under a new id first"],"tags":["fastapi","http-403","skills","readonly","authorization"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}