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
- Read the (added=... removed=...) detail - it names exactly which tool names moved
- If the change is unintended, revert the tool-catalog or prompt edit that altered the surface
- 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)
- 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
- Any PR that adds, removes, or renames a tool must touch scripts/runtime-contract-budget.json in the same commit
- Re-measure on Linux CI before updating identities; other platforms can render different bytes
- Edits to BASE_PROMPT re-hash every representative stage - budget the digest updates alongside the prompt change
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
- {kind} field `{dotted_names}` must be sorted unique non-empt
- {kind} metric `{'.'.join((*base, 'tools'))}` must equal the
- {kind} field `{'.'.join((*base, 'identity_sha256'))}` must m
- baseline provenance build profile/sample count changed
- baseline receipt {field} does not match baseline_observation
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/8171fed962295db4.
Report an issue: GitHub.