JuliusBrussee/caveman · error

${label}:${lineNo}: duplicate key "${entry.key}"

Error message

${label}:${lineNo}: duplicate key "${entry.key}"

What it means

Thrown by the catalog parser when the same key appears twice at indent 2 within one provider row. Because the parser builds a plain object per row, a duplicate key would silently overwrite the earlier value; rejecting it preserves the source's integrity and makes the generated catalog deterministic.

Source

Thrown at scripts/generate-agent-catalog.mjs:75

    const indent = shape[1].length;
    const dash = shape[2] !== undefined;
    const rest = shape[3];
    if (indent === 0) {
      if (!dash) throw new Error(`${label}:${lineNo}: expected a top-level "- provider: ..." row`);
      row = {};
      rows.push(row);
      blockKey = null;
      const entry = splitKeyValue(rest, label, lineNo);
      if (entry.value === undefined) throw new Error(`${label}:${lineNo}: row must start with an inline "provider" value`);
      row[entry.key] = entry.value;
      continue;
    }
    if (row === null) throw new Error(`${label}:${lineNo}: indented line before any row`);
    if (indent === 2) {
      if (dash) throw new Error(`${label}:${lineNo}: unexpected sequence item at row level`);
      const entry = splitKeyValue(rest, label, lineNo);
      if (Object.prototype.hasOwnProperty.call(row, entry.key)) {
        throw new Error(`${label}:${lineNo}: duplicate key "${entry.key}"`);
      }
      if (entry.value === undefined) {
        blockKey = entry.key;
        row[entry.key] = undefined;
      } else {
        blockKey = null;
        row[entry.key] = entry.value;
      }
      continue;
    }
    if (indent === 4) {
      if (blockKey === null) throw new Error(`${label}:${lineNo}: nested line without an open block`);
      if (dash) {
        if (row[blockKey] === undefined) row[blockKey] = [];
        if (!Array.isArray(row[blockKey])) throw new Error(`${label}:${lineNo}: "${blockKey}" mixes map and sequence entries`);
        row[blockKey].push(rest);
        continue;
      }

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Search the current row (from its "- provider:" line to the next one) for the duplicate key named in the error and delete or rename one occurrence.
  2. If you meant to have multiple values, use the two-level block form: " key:" with " - value" items.

Example fix

# before
- provider: anthropic
  model: claude-4
  model: claude-haiku

# after
- provider: anthropic
  models:
    - claude-4
    - claude-haiku
Defensive patterns

Strategy: validation

Validate before calling

function noDuplicateRowKeys(text) {
  const seen = new Set(); let row = 0;
  for (const line of text.split("\n")) {
    if (/^- provider: /.test(line)) seen.clear();
    const m = /^ {2}([A-Za-z_][A-Za-z0-9_]*):/.exec(line);
    if (m) { if (seen.has(m[1])) return false; seen.add(m[1]); }
  }
  return true;
}

Try / catch

try {
  parseCatalog(text);
} catch (err) {
  if (/duplicate key "/.test(err.message) && !err.message.includes(".")) {
    // remove or rename the duplicated indent-2 key in the current row
  } else throw err;
}

Prevention

When it happens

Trigger: Adding a property to a row that already defines it, e.g. two "model:" lines under one provider; pasting a block that re-declares keys already present higher in the row.

Common situations: Appending new config without scrolling to check the row's existing keys; merge conflicts resolved by keeping both sides' lines.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/95f2694dac68f724. Report an issue: GitHub.