zylon-ai/private-gpt · error · ValueError

First message in the block must be a USER message.

Error message

First message in the block must be a USER message.

What it means

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.

Source

Thrown at private_gpt/chat/input_models.py:719

        for block in user_blocks:
            if not block:
                continue

            # Find TLDR messages and their positions in this block
            tldr_positions: list[int] = []
            for j, message in enumerate(block):
                if _has_tldr_content(message):
                    tldr_positions.append(j)

            if not tldr_positions:
                # No TLDR in this block, keep all messages as-is
                add_by_id(block)
                continue

            user_message: ChatMessage = block[0]
            if user_message and user_message.role != MessageRole.USER:
                raise ValueError("First message in the block must be a USER message.")

            # Group consecutive TLDR positions together so we can detect accumulated
            # TLDRBlocks (where each subsequent block repeats all prior entries plus
            # new ones at the end).
            tldr_positions_grouped_by_consecutive = [
                [num for _, num in group]
                for _, group in groupby(
                    enumerate(tldr_positions), lambda x: x[1] - x[0]
                )
            ]

            # Tracks how many TLDR ChatMessages were emitted by the previous group.
            # Accumulated right-side TLDRBlocks re-emit all prior summaries as a
            # prefix, so we skip that prefix to avoid duplicates while still
            # preserving distinct instances with identical text (e.g. TOOL("-")).
            prev_group_tldr_count = 0

            for i, pos in enumerate(tldr_positions_grouped_by_consecutive):

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Do not expose delete actions for skills where the API reports readonly=true.
  2. If removal is genuinely required, ask an operator to remove/flag the skill at the storage/admin level rather than the API.
  3. Check the skill metadata first (GET) and branch on skill.readonly.

Example fix

// before
await skillsApi.delete(skill.id, collection);

// after
if (skill.readonly) { toast('Readonly skill cannot be deleted'); return; }
await skillsApi.delete(skill.id, collection);
Defensive patterns

Strategy: type-guard

Validate before calling

const skill = await skillsApi.get(id, collection);
if (skill?.readonly) { throw new ReadOnlySkillError(id); }

Type guard

const isDeletableSkill = (s) => s != null && !s.readonly;

Try / catch

try { await skillsApi.delete(id, collection); }
catch (e) {
  if (e.status === 403 && /readonly/.test(e.detail)) { /* hide delete UI */ }
  else throw e;
}

Prevention

When it happens

Trigger: Attempting to delete a builtin/system skill flagged readonly in its collection; a UI that shows readonly skills with an active delete button.

Common situations: Trying to remove bundled skills shipped with the deployment; permission model changes that marked existing skills readonly after an upgrade.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/eb91e50d195f73df. Report an issue: GitHub.