JuliusBrussee/caveman · error

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

Error message

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

What it means

Thrown by the catalog parser when the same sub-key appears twice inside one indent-4 map block. Like the row-level duplicate-key check, it exists because the parser writes into a plain object; a duplicate would silently clobber the earlier value and make generated output depend on line order.

Source

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

      }
      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;
      }
      if (row[blockKey] === undefined) row[blockKey] = {};
      if (Array.isArray(row[blockKey]) || typeof row[blockKey] !== "object") {
        throw new Error(`${label}:${lineNo}: "${blockKey}" mixes map and sequence entries`);
      }
      const entry = splitKeyValue(rest, label, lineNo);
      if (entry.value === undefined) throw new Error(`${label}:${lineNo}: nesting deeper than two levels is not supported`);
      if (Object.prototype.hasOwnProperty.call(row[blockKey], entry.key)) {
        throw new Error(`${label}:${lineNo}: duplicate key "${blockKey}.${entry.key}"`);
      }
      row[blockKey][entry.key] = entry.value;
      continue;
    }
    throw new Error(`${label}:${lineNo}: unexpected indent of ${indent} spaces`);
  }
  return rows;
}

function splitKeyValue(text, label, lineNo) {
  const match = /^([A-Za-z_][A-Za-z0-9_]*):(?: (.*))?$/.exec(text);
  if (match === null) throw new Error(`${label}:${lineNo}: cannot read "${text}" as a "key: value" pair`);
  return { key: match[1], value: match[2] === undefined ? undefined : scalar(match[2], label, lineNo) };
}

function scalar(text, label, lineNo) {
  if (text === "") throw new Error(`${label}:${lineNo}: empty value`);
  if (text === "null") return null;

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. In the block named by blockKey in the message, find the two occurrences of entry.key and delete or rename one.
  2. If both values are needed, rename to distinct keys (e.g. default_v1/default_v2) or restructure as a list where duplicates are legitimate distinct items.

Example fix

# before
- provider: anthropic
  endpoints:
    default: /v1/messages
    default: /v1/count_tokens

# after
- provider: anthropic
  endpoints:
    messages: /v1/messages
    count_tokens: /v1/count_tokens
Defensive patterns

Strategy: validation

Validate before calling

function noDuplicateBlockKeys(text) {
  const blocks = new Map();
  for (const line of text.split("\n")) {
    const header = /^ {2}([A-Za-z_][A-Za-z0-9_]*):\s*$/.exec(line.replace(/\s+$/, ""));
    if (header) { blocks.set(header[1], new Set()); continue; }
    const entry = /^ {4}([A-Za-z_][A-Za-z0-9_]*):/.exec(line);
    if (entry) {
      for (const [name, set] of blocks) { if (set.has(entry[1])) return false; set.add(entry[1]); break; }
    }
  }
  return true;
}

Try / catch

try {
  parseCatalog(text);
} catch (err) {
  if (/duplicate key ".*\./.test(err.message)) {
    // the message gives blockKey.entryKey — dedupe within that block
  } else throw err;
}

Prevention

When it happens

Trigger: Within one block (e.g. " endpoints:"), writing " default: /a" twice, or pasting a block that re-declares a sub-key already present earlier in the same block.

Common situations: Merging edits where both branches added the same sub-key; duplicating a neighboring line as a template and forgetting to rename the key.

Related errors


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