JuliusBrussee/caveman · error

assembly slot id must be non-empty and unique

Error message

assembly slot id must be non-empty and unique: ${JSON.stringify(slot.id)}

What it means

Every slot passed to assemble() must have a non-empty `id` that is unique across the slots array; slot ids feed the content-hash ledger and the emitted breakpoint names. assemble() collects ids into a Set and throws this Error as soon as a slot has an empty id or repeats an earlier id.

Solutions

  1. Give every slot a distinct, non-empty string id (e.g. "system", "history-0", "tools").
  2. Deduplicate before assembling: filter duplicate ids or suffix them ("system-2").
  3. If ids are generated, assert they are filled in before building the options object.
  4. Validate the slots array with a pre-check loop and fail early with a clear message identifying the offending entry.

Example fix

// before
const slots = [{ id: "system", stability: "stable", content }, ...extra.map(() => ({ id: "system", ... }))];

// after
const slots = [
  { id: "system", stability: "stable", content },
  ...extra.map((s, i) => ({ id: `extra-${i}`, ...s }))
];
const seen = new Set();
for (const s of slots) {
  if (!s.id || seen.has(s.id)) throw new Error(`bad slot id: ${JSON.stringify(s.id)}`);
  seen.add(s.id);
}
Defensive patterns

Strategy: validation

Validate before calling

function validateSlotIds(slots) {
  const seen = new Set();
  for (const s of slots) {
    if (typeof s.id !== "string" || s.id.length === 0) throw new Error(`slot id missing: ${JSON.stringify(s)}`);
    if (seen.has(s.id)) throw new Error(`duplicate slot id: ${s.id}`);
    seen.add(s.id);
  }
}

Type guard

function hasUniqueIds(slots) {
  return slots.every((s) => typeof s.id === "string" && s.id.length > 0)
    && new Set(slots.map((s) => s.id)).size === slots.length;
}

Try / catch

try {
  return cave.assemble({ ...opts, slots });
} catch (e) {
  if (e.message.includes("slot id must be non-empty and unique")) {
    // log which slot literal caused it and correct the generator
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing an AssemblySlot with `id: ""` or `id: undefined` (JS callers), or two slots with the same id — e.g. building slots in a loop with a constant id, or merging slot lists from two sources without deduplicating.

Common situations: Programmatic slot generation using a placeholder id that never got interpolated; concatenating per-feature slot arrays that both contain a "system" slot; an upstream refactor renaming slot fields so ids arrive empty; hand-written slot literals copy-pasted without changing the id.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/e78121bd4e925f2e. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/typescript/src/index.ts:1828

const assemblyHashLedgers = new WeakMap<Cave, Map<string, string>>();
const assemblyRequestHeaders = new WeakMap<object, string>();

function assemble(cave: Cave, options: AssembleOptions): AssemblyResult {
  const provider = options.provider.trim().toLowerCase();
  const emitCacheHints = options.emitCacheHints ?? "gateway";
  if (!options.model || !options.sessionId) throw new Error("assemble requires model and sessionId");
  if (!["gateway", "self", "none"].includes(emitCacheHints)) {
    throw new Error(`unknown emitCacheHints value ${JSON.stringify(emitCacheHints)}`);
  }

  const ids = new Set<string>();
  let ledger = assemblyHashLedgers.get(cave);
  if (!ledger) {
    ledger = new Map<string, string>();
    assemblyHashLedgers.set(cave, ledger);
  }
  const normalized = options.slots.map((slot) => {
    if (!slot.id || ids.has(slot.id)) throw new Error(`assembly slot id must be non-empty and unique: ${JSON.stringify(slot.id)}`);
    ids.add(slot.id);
    if (!["stable", "session", "volatile"].includes(slot.stability)) {
      throw new Error(`assembly slot ${JSON.stringify(slot.id)} has unknown stability ${JSON.stringify(slot.stability)}`);
    }
    let contentJSON: string;
    try {
      contentJSON = canonicalJSON(slot.content);
    } catch {
      throw new Error(`assembly slot ${JSON.stringify(slot.id)} content is not JSON-serializable`);
    }
    const contentHash = sha256Hex(contentJSON);
    if (slot.stability !== "volatile") {
      const key = `${options.sessionId}\0${slot.id}`;
      const previous = ledger.get(key);
      if (previous !== undefined && previous !== contentHash) throw new AssemblyStabilityError(slot.id);
      ledger.set(key, contentHash);
    }
    return { ...slot, content: JSON.parse(contentJSON) as unknown };

View on GitHub (pinned to 3ee70a1026)