Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "objectstate"
version = "1.0.23"
version = "1.1.0"
description = "Generic lazy dataclass configuration framework with dual-axis inheritance and contextvars-based resolution"
authors = [{name = "Tristan Simas", email = "tristan.simas@mail.mcgill.ca"}]
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion src/objectstate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
'ObjectStateEditSession',
]

__version__ = "1.0.23"
__version__ = "1.1.0"
__author__ = 'OpenHCS Team'
__description__ = 'Generic configuration framework for lazy dataclass resolution'

Expand Down
11 changes: 10 additions & 1 deletion src/objectstate/object_state_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2172,7 +2172,7 @@ def save_history_to_file(cls, filepath: str) -> None:

@classmethod
def load_history_from_file(cls, filepath: str) -> None:
"""Load a trusted type-preserving ObjectState history document.
"""Load and materialize a trusted ObjectState history document.

Args:
filepath: Path to the binary ObjectState history document.
Expand All @@ -2181,4 +2181,13 @@ def load_history_from_file(cls, filepath: str) -> None:

with open(filepath, "rb") as history_file:
cls.import_history_from_dict(dill.load(history_file))
restored_at_head = cls._current_head is None
snapshot_id = cls._current_snapshot_id()
if snapshot_id is not None:
if not cls.time_travel_to_snapshot(snapshot_id):
raise RuntimeError(
"Loaded ObjectState history could not restore its current snapshot."
)
if restored_at_head:
cls._current_head = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When histories saved at live head are loaded, time_travel_to_snapshot() fires history-change callbacks while _current_head is still set to the head snapshot; this line then resets it without firing another callback. UI subscribers that refresh from those callbacks can observe get_current_snapshot_index()/is_time_traveling() as time-traveling and never learn that the restored timeline is back at live head.

Severity: medium


🤖 Was this useful? React with 👍 or 👎

logger.info(f"⏱️ Loaded {len(cls._snapshots)} snapshots from {filepath}")
26 changes: 26 additions & 0 deletions tests/test_history_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,29 @@ def test_history_file_round_trips_typed_values(tmp_path: Path) -> None:
assert parameters["mode"] is HistoryMode.FAST
assert parameters["transform"] is identity
assert history_path.stat().st_size > 0


def test_history_file_materializes_current_snapshot_into_registered_state(
tmp_path: Path,
) -> None:
state = ObjectState(
HistoryConfig(
output_path=tmp_path / "before",
mode=HistoryMode.FAST,
transform=identity,
),
scope_id="restored_history",
)
ObjectStateRegistry.register(state, _skip_snapshot=True)
ObjectStateRegistry.record_snapshot("baseline", scope_id=state.scope_id)
state.update_parameter("output_path", tmp_path / "persisted")
ObjectStateRegistry.record_snapshot("persisted edit", scope_id=state.scope_id)
history_path = tmp_path / "history.objectstate"
ObjectStateRegistry.save_history_to_file(str(history_path))

state.update_parameter("output_path", tmp_path / "after-save")
ObjectStateRegistry.load_history_from_file(str(history_path))

assert state.parameters["output_path"] == tmp_path / "persisted"
assert state.to_resolved_object().output_path == tmp_path / "persisted"
assert ObjectStateRegistry.get_current_snapshot_index() == -1
Loading