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
- Give every slot a distinct, non-empty string id (e.g. "system", "history-0", "tools").
- Deduplicate before assembling: filter duplicate ids or suffix them ("system-2").
- If ids are generated, assert they are filled in before building the options object.
- 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
- Generate slot ids from indexed loops (`feature-${i}`) rather than constants.
- Deduplicate merged slot arrays before assembling.
- Keep a naming convention for slot ids and review slot literals in code review.
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
- assembly slot has unknown stability
- assemble requires model and session_id
- assemble requires model and sessionId
- assembly slot id must be non-empty and unique
- assembly slot content is not JSON-serializable
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)