@@ -383,8 +383,8 @@ def get_ancestor_objects(cls, scope_id: Optional[str], use_saved: bool = False)
383383 state = cls ._states .get (ancestor_key )
384384 if state :
385385 if use_saved :
386- # Return saved baseline (object_instance is updated in mark_saved )
387- objects .append (state .object_instance )
386+ # Return saved baseline (saved_object handles delegation correctly )
387+ objects .append (state .saved_object )
388388 else :
389389 # Return live state with current edits
390390 objects .append (state .to_object ())
@@ -420,7 +420,7 @@ def get_ancestor_objects_with_scopes(cls, scope_id: Optional[str], use_saved: bo
420420 for ancestor_key in ancestors :
421421 state = cls ._states .get (ancestor_key )
422422 if state :
423- obj = state .object_instance if use_saved else state .to_object ()
423+ obj = state .saved_object if use_saved else state .to_object ()
424424 results .append ((ancestor_key , obj ))
425425
426426 return results
@@ -1563,6 +1563,18 @@ def context_obj(self) -> Optional[Any]:
15631563 """Derive context_obj from parent_state (no separate attribute needed)."""
15641564 return self ._parent_state .object_instance if self ._parent_state else None
15651565
1566+ @property
1567+ def saved_object (self ) -> Any :
1568+ """Get the saved baseline object with the correct type.
1569+
1570+ For delegation: returns _extraction_target (the delegate/config)
1571+ For non-delegation: returns object_instance
1572+
1573+ This is the object that should be used for context resolution when
1574+ use_saved=True. It represents the "saved" state of the editable object.
1575+ """
1576+ return self ._extraction_target
1577+
15661578 @property
15671579 def fields (self ) -> FieldProxy :
15681580 """Type-safe field access via FieldProxy.
@@ -2282,7 +2294,8 @@ def _compute_resolved_snapshot(self, use_saved: bool = False) -> Dict[str, Any]:
22822294
22832295 # Use saved baseline or live state for this object
22842296 if use_saved :
2285- current_obj = self .object_instance
2297+ # Use saved_object which handles delegation correctly
2298+ current_obj = self .saved_object
22862299 else :
22872300 # CRITICAL: Use to_object() to get CURRENT state with user edits,
22882301 # not object_instance which is the original/saved baseline.
@@ -2392,16 +2405,17 @@ def mark_saved(self) -> None:
23922405 if is_container :
23932406 continue
23942407
2395- # Get the old value from object_instance by navigating dotted path
2408+ # Get the old value by navigating dotted path on the extraction target
2409+ # For delegation, parameters are on the delegate, not object_instance
23962410 try :
23972411 # Navigate through nested attributes for dotted paths
2398- obj = self .object_instance
2412+ obj = self ._extraction_target
23992413 parts = param_name .split ('.' )
24002414 for part in parts :
24012415 obj = object .__getattribute__ (obj , part )
24022416 old_instance_values [param_name ] = obj
24032417 except AttributeError :
2404- # Field doesn't exist on object_instance , skip it
2418+ # Field doesn't exist on extraction target , skip it
24052419 pass
24062420
24072421 # Find parameters that differ between old object_instance and new live parameters
@@ -2419,12 +2433,18 @@ def mark_saved(self) -> None:
24192433 if old_value != new_value :
24202434 changed_params .append (param_name )
24212435
2422- # CRITICAL: Rebuild object_instance BEFORE invalidating descendants
2423- # Descendants will recompute using parent's object_instance , so it must have new values!
2436+ # CRITICAL: Rebuild extraction target BEFORE invalidating descendants
2437+ # Descendants will recompute using parent's extraction target , so it must have new values!
24242438 if not isinstance (self .object_instance , type ):
2425- # Update object_instance with current parameters
2426- # to_object() already handles all types uniformly
2427- self .object_instance = self .to_object ()
2439+ if self ._delegate_attr is not None :
2440+ # DELEGATION: to_object() returns the delegate and updates it on object_instance
2441+ # as a side effect. Keep object_instance unchanged (it's the lifecycle object).
2442+ # _extraction_target is updated to point to the new delegate.
2443+ self ._extraction_target = self .to_object ()
2444+ else :
2445+ # NON-DELEGATION: to_object() returns the reconstructed object_instance
2446+ self .object_instance = self .to_object ()
2447+ self ._extraction_target = self .object_instance # Keep in sync
24282448
24292449 # Update saved parameters (after object_instance update, before invalidation)
24302450 self ._saved_parameters = copy .deepcopy (self .parameters )
@@ -2624,8 +2644,15 @@ def to_object(self) -> Any:
26242644 - Python functions: can't copy, return original
26252645 - Everything else: shallow copy + reconstruct nested dataclass fields
26262646
2627- DELEGATION: If __objectstate_delegate__ was used, reconstructs the delegate
2628- and updates it on the original object_instance, returning object_instance.
2647+ DELEGATION: If __objectstate_delegate__ was used:
2648+ - Reconstructs the delegate (e.g., pipeline_config)
2649+ - Updates the delegate attribute on object_instance as a side effect
2650+ - Returns the reconstructed delegate (NOT object_instance)
2651+ - Callers needing the lifecycle object (orchestrator) should use state.object_instance
2652+
2653+ Returns:
2654+ The reconstructed object that matches the stored parameters.
2655+ For delegation, this is the delegate type (config), not the lifecycle object.
26292656 """
26302657 if self ._cached_object is not None :
26312658 return self ._cached_object
@@ -2694,10 +2721,13 @@ def to_object(self) -> Any:
26942721 reconstructed = obj_copy
26952722
26962723 # DELEGATION: If using delegation, update the delegate attribute on object_instance
2697- # and return the object_instance (which now has the updated delegate)
2724+ # as a side effect, but return the reconstructed delegate (not object_instance).
2725+ # This ensures callers get the correct type (config, not orchestrator).
2726+ # Callers who need the lifecycle object should access state.object_instance directly.
26982727 if self ._delegate_attr is not None :
26992728 setattr (self .object_instance , self ._delegate_attr , reconstructed )
2700- self ._cached_object = self .object_instance
2729+ # Return the reconstructed delegate - this is what the parameters represent
2730+ self ._cached_object = reconstructed
27012731 else :
27022732 self ._cached_object = reconstructed
27032733
0 commit comments