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

  1. Mark content that evolves per turn as "volatile" — only truly immutable content gets stable/session
  2. When stable content legitimately changes, mint a new sessionId (new conversation/epoch) so the ledger resets
  3. Audit stable slots for hidden nondeterminism (timestamps, random ids, iteration order of non-canonical structures)
  4. 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

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


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)