JuliusBrussee/caveman · error · AssemblyStabilityError
assembly slot changed after being declared stable
Error message
assembly slot ${JSON.stringify(slotId)} changed after being declared stable What it means
Thrown as AssemblyStabilityError when a slot declared stable or session produces a different canonical content hash than a previous assemble() call with the same sessionId on the same Cave instance (in-process WeakMap ledger keyed `sessionId\0slotId`). The guard exists because stable prefixes are reused for provider prompt caching — mutating them silently would poison the cache.
Solutions
- Mark content that evolves per turn as "volatile" — only truly immutable content gets stable/session
- When stable content legitimately changes, mint a new sessionId (new conversation/epoch) so the ledger resets
- Audit stable slots for hidden nondeterminism (timestamps, random ids, iteration order of non-canonical structures)
- Catch AssemblyStabilityError and re-assemble with a fresh sessionId as a runtime escape hatch (see tryCatchPattern)
Example fix
// before
slots.push({ id: "system", stability: "stable", content: `Assistant. Today is ${new Date().toISOString()}.` }); // changes every call → throws on turn 2
// after
slots.push({ id: "system", stability: "stable", content: "Assistant." });
slots.push({ id: "clock", stability: "volatile", content: new Date().toISOString() }); Defensive patterns
Strategy: type-guard
Validate before calling
function assertStableSlotImmutable(previousJSON: string | undefined, slot: AssemblySlot): void {
if (slot.stability === "volatile" || previousJSON === undefined) return;
if (JSON.stringify(slot.content) !== previousJSON) {
throw new Error(`slot ${slot.id} would violate its stability declaration`);
}
} Type guard
import { AssemblyStabilityError } from "@caveman-ai/sdk";
function isAssemblyStabilityError(e: unknown): e is AssemblyStabilityError {
return e instanceof AssemblyStabilityError || (e instanceof Error && e.name === "AssemblyStabilityError");
} Try / catch
try {
assembly = cave.assemble({ provider, model, sessionId, slots });
} catch (e) {
if (isAssemblyStabilityError(e)) {
// Legitimate content change → new conversation epoch resets the ledger
assembly = cave.assemble({ provider, model, sessionId: `${sessionId}#${++epoch}`, slots });
} else throw e;
} Prevention
- Only mark content stable if it is byte-identical for the whole session — everything else is volatile
- Audit stable slots for hidden nondeterminism (timestamps, random ids, locale formatting)
- Hash stable slot content in dev builds and assert equality per turn before assemble() does
When it happens
Trigger: Editing the "system" or "tools" slot between turns while reusing sessionId; appending to a stable slot each turn (ever-growing system prompt); a tools list marked stable but rebuilt from mutable state that drifted; same Cave reused across conversations with colliding sessionIds.
Common situations: Prompt templates with timestamps or rolling context accidentally inside a stable slot; tool catalogs filtered per-user but declared stable; sessionId not actually unique per conversation so ledgers collide.
Related errors
- assemble requires model and session_id
- assemble requires model and sessionId
- assembly slot id must be non-empty and unique
- assembly slot id must be non-empty and unique
- assembly slot content is not JSON-serializable
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/562d0e6ac69cccc1.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/typescript/src/index.ts:1831
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 };
});
const ordered = (["stable", "session", "volatile"] as const)
.flatMap((stability) => normalized.filter((slot) => slot.stability === stability));
const prefixSlots = ordered.filter((slot) => slot.stability !== "volatile");
const volatileSlots = ordered.filter((slot) => slot.stability === "volatile");
let request: Record<string, unknown>;
let prefixBytes: string;
let breakpoints: string[] = [];
if (provider === "anthropic") {
const toolsSlot = prefixSlots.find((slot) => slot.id === "tools" && Array.isArray(slot.content));
const systemSlots = prefixSlots.filter((slot) => slot !== toolsSlot);
const system = systemSlots.map((slot) => ({ type: "text", text: assemblyText(slot.content) }));View on GitHub (pinned to 766dce6b13)