Skip to content

bug(memory): MessagePart::Summary deserialization fails for pre-v0.17.1 SQLite history #2278

@bug-ops

Description

@bug-ops

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

  1. Use any Zeph version before v0.17.1 for at least one session that triggers compaction/summarization.
  2. Upgrade to v0.17.1.
  3. 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")] on MessagePart
  • crates/zeph-memory/src/sqlite/messages/mod.rs:52-64deserialize_parts() — silent empty fallback, no compat path
  • crates/zeph-memory/src/sqlite/ — no migration for this format change

Metadata

Metadata

Assignees

Labels

P1High ROI, low complexity — do next sprintbugSomething isn't workingmemoryzeph-memory crate (SQLite)

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions