moeru-ai/airi · error · Error

Invalid message example format: ${line}

Error message

Invalid message example format: ${line}

What it means

newAiriCard parses SillyTavern V3 character cards (ccv3): mes_example is split on the literal '<START>\n' and every remaining line must start with '{{char}}:' or '{{user}}:'. Any other line — a blank line, narration text, or a block produced by a '<START>' separator with different whitespace or line endings — throws with the offending line in the message. This makes loosely-authored real-world cards fail import outright.

Source

Thrown at packages/stage-ui/src/stores/modules/airi-card.ts:424

        notesMultilingual: ccv3Card.data.creator_notes_multilingual,
        personality: ccv3Card.data.personality ?? '',
        scenario: ccv3Card.data.scenario ?? '',
        greetings: [
          ccv3Card.data.first_mes,
          ...(ccv3Card.data.alternate_greetings ?? []),
        ],
        greetingsGroupOnly: ccv3Card.data.group_only_greetings ?? [],
        systemPrompt: ccv3Card.data.system_prompt ?? '',
        postHistoryInstructions: ccv3Card.data.post_history_instructions ?? '',
        messageExample: ccv3Card.data.mes_example
          ? ccv3Card.data.mes_example
              .split('<START>\n')
              .filter(Boolean)
              .map(example => example.split('\n')
                .map((line) => {
                  if (line.startsWith('{{char}}:') || line.startsWith('{{user}}:'))
                    return line as `{{char}}: ${string}` | `{{user}}: ${string}`
                  throw new Error(`Invalid message example format: ${line}`)
                }))
          : [],
        tags: ccv3Card.data.tags ?? [],
        extensions: {
          ...ccv3Card.data.extensions,
          airi: resolveAiriExtension(ccv3Card),
        },
      }
    }

    return {
      ...card,
      extensions: {
        ...card.extensions,
        airi: resolveAiriExtension(card),
      },
    }
  }

View on GitHub (pinned to f679616c34)

Solutions

  1. Pre-normalize mes_example before import: split on /<START>\r?\n/, trim lines, and drop or fix lines that lack the {{char}}:/{{user}}: prefix.
  2. Edit the card JSON directly and make every line inside mes_example start with '{{char}}:' or '{{user}}:'.
  3. Wrap the import in try/catch and show the offending line (already included in the error message) to the user.
  4. File an issue or relax the parser upstream (skip/blank-tolerant parsing) if strictness is not a product requirement.

Example fix

// before
const card = newAiriCard(ccv3)

// after - normalize before parsing so strict line rules pass
function normalizeMesExample(raw: string | undefined): string | undefined {
  if (!raw) return undefined
  const cleaned = raw
    .split(/<START>\r?\n/)
    .filter(Boolean)
    .map(block => block
      .split(/\r?\n/)
      .filter(line => line.startsWith('{{char}}:') || line.startsWith('{{user}}:'))
      .join('\n'))
    .filter(Boolean)
    .join('<START>\n')
  return cleaned || undefined
}

const card = newAiriCard({
  ...ccv3,
  data: { ...ccv3.data, mes_example: normalizeMesExample(ccv3.data.mes_example) },
})
Defensive patterns

Strategy: validation

Validate before calling

function hasValidMesExample(mesExample: string | undefined): boolean {
  if (!mesExample) return true // absent is fine
  return mesExample
    .split('<START>\n')
    .filter(Boolean)
    .every(block => block
      .split('\n')
      .every(line => line.startsWith('{{char}}:') || line.startsWith('{{user}}:')))
}

if (!hasValidMesExample(ccv3.data.mes_example))
  ccv3 = normalizeCardBeforeImport(ccv3) // strip/skip offending lines
const card = newAiriCard(ccv3)

Type guard

function isParsableMesExampleLine(line: string): line is `{{char}}: ${string}` | `{{user}}: ${string}` {
  return line.startsWith('{{char}}:') || line.startsWith('{{user}}:')
}

Try / catch

try {
  const card = newAiriCard(ccv3)
}
catch (error) {
  const message = errorMessageFrom(error) ?? ''
  if (message.startsWith('Invalid message example format:')) {
    // the offending line is embedded in the message; show it for repair
    showImportError(`Fix the example dialog line: ${message.split(': ').slice(1).join(': ')}`)
    return
  }
  throw error
}

Prevention

When it happens

Trigger: Importing a card whose example dialogs contain blank lines between turns or narration lines; mes_example using CRLF so the split on '<START>\n' leaves stray '\r' or unmatched separators; examples separated by '<START>' without the required trailing newline; hand-edited or third-party tool exports with free-form text inside examples.

Common situations: Downloading community character cards that render fine in SillyTavern but contain formatting the strict parser rejects; authors pasting dialogue with empty lines; cards authored on Windows editors inserting CRLF.

Related errors


AI-assisted analysis of moeru-ai/airi@f679616c34 (2026-08-18). Data as JSON: /api/errors/727ef4608f6b829f. Report an issue: GitHub.