{"record":{"id":"2bd34a3739a6eb84","repo":"JuliusBrussee/caveman","slug":"caveman-recovery-metadata-is-immutable","errorCode":null,"errorMessage":"Caveman recovery metadata is immutable","messagePattern":"Caveman recovery metadata is immutable","errorType":"exception","errorClass":"AttributeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/llama_index.py","lineNumber":95,"sourceCode":"\n\n_invocation = contextvars.ContextVar(\"caveman_llama_index_invocation\", default=None)\n\n\nclass _RecoveryMetadata(ToolMetadata):\n    def get_parameters_dict(self):\n        # Default ToolMetadata removes additionalProperties from the schema.\n        return copy.deepcopy(RECOVERY_SCHEMA)\n\n\nclass _FrozenRecoveryMetadata(_RecoveryMetadata):\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        object.__setattr__(self, \"_caveman_frozen\", True)\n\n    def __setattr__(self, name, value):\n        if getattr(self, \"_caveman_frozen\", False):\n            raise AttributeError(\"Caveman recovery metadata is immutable\")\n        super().__setattr__(name, value)\n\n\nclass _FrozenFunctionTool(FunctionTool):\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        object.__setattr__(self, \"_caveman_frozen\", True)\n\n    def __setattr__(self, name, value):\n        if getattr(self, \"_caveman_frozen\", False):\n            raise AttributeError(\"Caveman recovery tool is immutable\")\n        super().__setattr__(name, value)\n\n\nclass _Recovery:\n    def __init__(self, runtime, scope):\n        self.runtime, self.scope = runtime, scope\n","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/llama_index.py#L77-L113","documentation":"_FrozenModel subclasses LlamaIndex's Pydantic BaseModel and sets a private flag after init; any subsequent attribute assignment on the instance raises AttributeError because recovery metadata objects must not be mutated after construction. This protects the bookkeeping state the caveman runtime relies on.","triggerScenarios":"Assigning any attribute (e.g. recovery_obj.some_field = x or recovery_obj.new_attr = 1) on a caveman-created recovery metadata/model instance after it has been constructed.","commonSituations":"Patching tool metadata in place, applying generic middleware that copies/rewrites attributes on LlamaIndex objects, or framework code that updates model fields post-init.","solutions":["Treat the object as read-only; construct a new instance with the desired values instead of mutating","Use object.__setattr__(obj, name, value) only if you explicitly must bypass the freeze (not recommended)","If you own the wrapping code, extract needed values at construction time and keep your own mutable copy"],"exampleFix":"// before\nrecovery_meta.name = \"renamed\"\n// after\nnew_meta = type(recovery_meta)(**{**recovery_meta.model_dump(), \"name\": \"renamed\"})","handlingStrategy":"validation","validationCode":"def can_set(obj, name):\n    return not getattr(obj, \"_caveman_frozen\", False)\n# check before assigning\nif not can_set(recovery_meta, \"name\"):\n    recovery_meta = type(recovery_meta)(**{**recovery_meta.model_dump(), \"name\": new_name})","typeGuard":"def is_frozen(obj) -> bool:\n    return getattr(obj, \"_caveman_frozen\", False)","tryCatchPattern":null,"preventionTips":["Treat caveman-created metadata objects as read-only","Copy via model_dump() when you need a modified version","Avoid generic middleware that mutates tool/model attributes in place"],"tags":["python","immutability","llamaindex"],"backgroundTag":"unsupported-operation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}