-
Notifications
You must be signed in to change notification settings - Fork 2
bug(memory): MessagePart::Summary deserialization fails for pre-v0.17.1 SQLite history #2278
Description
Problem
After the fix(memory): use MessagePart::Summary for summary serialization change (#2271, released in v0.17.1), existing SQLite message history is silently corrupted on load. Any message that was stored with the old Summary format fails to deserialize and is replaced with an empty parts list.
80 failures observed in a single session log (zeph-acp.2026-03-27.log).
Root Cause
MessagePart gained #[serde(tag = "kind", rename_all = "snake_case")] in #2271, changing the wire format:
| Version | SQLite JSON stored |
|---|---|
| pre-v0.17.1 | [{"Summary":{"text":"..."}}] (externally tagged — serde default) |
| v0.17.1+ | [{"kind":"summary","text":"..."}] (internally tagged) |
The new deserializer requires a kind field at the top level. Old records don't have it. serde_json::from_str returns missing field 'kind' and sqlite/messages/mod.rs:62 silently returns vec![].
No migration was added. No backward-compat fallback exists.
Impact
Every user upgrading from pre-v0.17.1 loses all Summary messages from their conversation history on the first startup after the upgrade. This degrades:
- Context continuity (summaries that bridged compacted history are gone)
- Token budget calculations (compaction may fire prematurely without accurate summary context)
- Memory tier promotion (summaries from promoted memories are lost)
Reproduction
- Use any Zeph version before v0.17.1 for at least one session that triggers compaction/summarization.
- Upgrade to v0.17.1.
- Start the agent — observe
WARN zeph_memory::sqlite::messages: failed to deserialize message parts, falling back to empty ... error=missing field 'kind'in logs.
Suggested Fix
Two complementary approaches:
Option A — SQLite migration (permanent fix): add a migration step in zeph-memory that rewrites all parts_json cells containing {"Summary":...} (externally-tagged) to the new internally-tagged format. Applies once at startup.
Option B — Compat deserializer (defensive fallback): in deserialize_parts(), if serde_json::from_str::<Vec<MessagePart>> fails, attempt a second pass that recognizes externally-tagged variants and converts them to the current format.
Both are needed: migration handles the existing DB, compat deserializer handles any other format divergence in future.
Files
crates/zeph-llm/src/provider.rs:174-175—#[serde(tag = "kind")]onMessagePartcrates/zeph-memory/src/sqlite/messages/mod.rs:52-64—deserialize_parts()— silent empty fallback, no compat pathcrates/zeph-memory/src/sqlite/— no migration for this format change