Hmbown/CodeWhale · error · RuntimeContractError

identity changed for {label} [`{'.'.join(path)}`]{detail}

Error message

identity changed for {label} [`{'.'.join(path)}`]{detail}

What it means

The core ratchet trip in compare(): for every identity path (tool surface_profile, per-mode/surface tool_names and identity_sha256, per-stage digests) the receipt must equal the budget exactly. Any add/remove/rename of a tool or any edit to prompt text that feeds a stage digest breaks byte equality and fails the run even when all numeric ceilings are respected. List values get an (added=... removed=...) detail naming the moved tool names; the CLI exits non-zero, so CI fails.

Source

Thrown at scripts/check-runtime-contract-budget.py:322

def compare(
    receipt: dict[str, Any], budget: dict[str, Any]
) -> tuple[list[MetricResult], list[MetricResult]]:
    """Return (increases, decreases) as path/label/current/ceiling tuples."""
    validate_receipt(receipt)
    validate_budget(budget)
    for path, label in IDENTITIES:
        receipt_value = required_value(receipt, path, "receipt")
        budget_value = required_value(budget, path, "budget")
        if receipt_value != budget_value:
            detail = ""
            if isinstance(receipt_value, list) and isinstance(budget_value, list):
                added = [str(item) for item in receipt_value if item not in budget_value]
                removed = [
                    str(item) for item in budget_value if item not in receipt_value
                ]
                detail = f" (added={added} removed={removed})"
            raise RuntimeContractError(
                f"identity changed for {label} [`{'.'.join(path)}`]{detail}"
            )
    increases: list[MetricResult] = []
    decreases: list[MetricResult] = []
    for path, label in METRICS:
        current = metric_value(receipt, path, "receipt")
        ceiling = metric_value(budget, path, "budget")
        result = (".".join(path), label, current, ceiling)
        if current > ceiling:
            increases.append(result)
        elif current < ceiling:
            decreases.append(result)
    return increases, decreases


def set_path_value(document: dict[str, Any], path: MetricPath, value: Any) -> None:
    target = document
    for part in path[:-1]:

View on GitHub (pinned to 8880682c63)

Solutions

  1. Read the (added=... removed=...) detail - it names exactly which tool names moved
  2. If the change is unintended, revert the tool-catalog or prompt edit that altered the surface
  3. If intended, hand-update the affected tool_names, tools/bytes/tokens_est ceilings, and identity_sha256 values together in scripts/runtime-contract-budget.json and justify the raise in _comment (the file's existing convention for explicit maintainer decisions)
  4. Re-measure on Linux CI before committing the budget change so platform drift cannot fake an identity change

Example fix

// recording an intentional tool addition in scripts/runtime-contract-budget.json:
// tool_names: [... "agent" ...]  (kept sorted)
// tools/bytes/tokens_est: raised to the re-measured values
// identity_sha256: recomputed over the new sorted names
// _comment: extend with the rationale, mirroring the existing v0.9.8 entries
Defensive patterns

Strategy: validation

Validate before calling

def identity_paths():
    paths = [("tool_catalog", "surface_profile")]
    for mode in ("plan", "act", "operate"):
        for surface in ("full", "active"):
            paths.append(("tool_catalog", "modes", mode, surface, "tool_names"))
            paths.append(("tool_catalog", "modes", mode, surface, "identity_sha256"))
    for stage in ("base", "project", "instructions", "skill", "memory", "goal", "handoff"):
        paths.append(("representative_context", "stages", stage, "identity_sha256"))
    return paths


def get(doc, path):
    for part in path:
        doc = doc.get(part, {}) if isinstance(doc, dict) else {}
    return doc


def identity_drift(receipt: dict, budget: dict) -> list[str]:
    drifted = []
    for path in identity_paths():
        if get(receipt, path) != get(budget, path):
            drifted.append(".".join(path))
    return drifted  # empty means compare() will not raise error 209

Prevention

When it happens

Trigger: Adding, removing, or renaming a tool in the production catalog (the budget _comment records exactly this for the v0.9.8 `agent` tool); editing BASE_PROMPT in crates/tui/src/prompts/text.rs, which re-hashes every representative stage; measuring on a platform whose rendering differs from the Linux CI that produced the budget.

Common situations: A PR that legitimately extends the tool surface and forgets this is a two-part decision (code + budget); cross-platform measurement drift (the budget _comment explicitly says re-measure on Linux CI if Lint reports identity drift); refactors that rename a FragmentId or stage key.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/8171fed962295db4. Report an issue: GitHub.