Description
What happened? With the default serialization_format="json", FileHistoryProvider.save_messages() skips history lines it cannot parse instead of reporting them. The append returns successfully, and because the unreadable message is treated as absent from the deduplication set, a replayed transcript writes that message into the file a second time. The same file cannot be read back: get_messages() raises on the very line the append path skipped.
What did I expect? The two formats and the two directions to agree. The MessagePack branch already reads through self._read_msgpack_messages() and propagates the error, and _read_json_messages() propagates it too. Only the JSON branch inlines its own parse loop, which catches Exception and calls logger.debug("failed to parse history line for deduplication") before continue.
Steps to reproduce
save_messages() two messages to a session with the default JSON format.
- Leave a line in the
.jsonl file that the reader cannot deserialize — a torn final line if the process dies mid-append, a file edited by hand, or a payload shape written by another version.
save_messages() the full transcript again.
- The call returns normally and the unreadable message is appended a second time, while
get_messages() raises ValueError: Failed to deserialize history line 2 from '.../s.jsonl'.
Code Sample
import asyncio, tempfile
from pathlib import Path
from agent_framework import Message
from agent_framework._sessions import FileHistoryProvider
async def main() -> None:
provider = FileHistoryProvider(Path(tempfile.mkdtemp()))
turn = [Message(role="user", contents=["hello"]), Message(role="assistant", contents=["hi there"])]
await provider.save_messages("s", turn)
path = provider._session_file_path("s")
lines = path.read_text(encoding="utf-8").splitlines()
path.write_text("\n".join([*lines[:-1], "{not json"]) + "\n", encoding="utf-8")
await provider.get_messages("s") # ValueError: Failed to deserialize history line 2
await provider.save_messages("s", turn) # returns normally; "hi there" is written again
asyncio.run(main())
Error Messages / Stack Traces
ValueError: Failed to deserialize history line 2 from '/tmp/.../s.jsonl'.
Package Versions
agent-framework-core: 1.19.0
Python Version
3.13.3
Additional Context
- The MessagePack equivalent already raises here, so the two branches of the same abstraction disagree.
self._read_msgpack_messages(file_path) on the write path versus an inlined try/except Exception: continue on the JSON path.
- Reusing
self._read_json_messages(file_path) for the deduplication read would make both branches and get_messages() share one parser and one error contract, which is where I would start.
- ADR 0034 lists "Fail before persistence when an object cannot be restored after a cold start" as a design principle, and describes failures as leaving the original file in place so an application fix or compatible reader can recover it — not as skipping records.
- I did not find a test covering either branch's behaviour on an unreadable record.
I am going to take this on unless you would rather handle it differently — happy to send a PR with a regression test covering both formats.
Description
What happened? With the default
serialization_format="json",FileHistoryProvider.save_messages()skips history lines it cannot parse instead of reporting them. The append returns successfully, and because the unreadable message is treated as absent from the deduplication set, a replayed transcript writes that message into the file a second time. The same file cannot be read back:get_messages()raises on the very line the append path skipped.What did I expect? The two formats and the two directions to agree. The MessagePack branch already reads through
self._read_msgpack_messages()and propagates the error, and_read_json_messages()propagates it too. Only the JSON branch inlines its own parse loop, which catchesExceptionand callslogger.debug("failed to parse history line for deduplication")beforecontinue.Steps to reproduce
save_messages()two messages to a session with the default JSON format..jsonlfile that the reader cannot deserialize — a torn final line if the process dies mid-append, a file edited by hand, or a payload shape written by another version.save_messages()the full transcript again.get_messages()raisesValueError: Failed to deserialize history line 2 from '.../s.jsonl'.Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.19.0
Python Version
3.13.3
Additional Context
self._read_msgpack_messages(file_path)on the write path versus an inlinedtry/except Exception: continueon the JSON path.self._read_json_messages(file_path)for the deduplication read would make both branches andget_messages()share one parser and one error contract, which is where I would start.I am going to take this on unless you would rather handle it differently — happy to send a PR with a regression test covering both formats.