diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 31dc8d00..f67a3a68 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -138,6 +138,8 @@ class Session: last_consolidated: int = 0 # Number of messages already consolidated to files def __post_init__(self) -> None: + if not isinstance(self.metadata, dict): + self.metadata = {} # An out-of-range offset (corrupt metadata) would hide all history; reset it. if ( isinstance(self.last_consolidated, bool) diff --git a/tests/session/test_consolidated_offset_clamp.py b/tests/session/test_consolidated_offset_clamp.py index 1bad7b1a..cb8a4122 100644 --- a/tests/session/test_consolidated_offset_clamp.py +++ b/tests/session/test_consolidated_offset_clamp.py @@ -58,3 +58,32 @@ def test_valid_offset_is_preserved(): session = _session(10, 4) assert session.last_consolidated == 4 assert len(session.get_history()) == 6 + + +def test_loaded_null_metadata_becomes_empty_dict(tmp_path: Path): + """Session jsonl metadata:null must load as {} so agent .pop/.get work.""" + manager = SessionManager(tmp_path) + path = manager._get_session_path("chan:chat") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps({ + "_type": "metadata", + "key": "chan:chat", + "created_at": "2026-01-01T00:00:00", + "updated_at": "2026-01-01T00:00:00", + "metadata": None, + "last_consolidated": 0, + }) + "\n", + encoding="utf-8", + ) + session = manager.get_or_create("chan:chat") + assert session.metadata == {} + session.metadata["title"] = "ok" + assert session.metadata["title"] == "ok" + session.metadata.pop("title", None) + assert session.metadata == {} + + +def test_session_post_init_coerces_null_metadata(): + session = Session(key="chan:chat", metadata=None) # type: ignore[arg-type] + assert session.metadata == {}