Skip to content

[Bug]: EventValue serialization fails with 'unknown variant' error #3260

@dariuszkowalski-com

Description

@dariuszkowalski-com

Fix: EventValue Serialization Bug

Problem

The error occurred when running forge conversation list because the database contained corrupted raw_content data in message 2 of conversation 0061bbde-d01e-4285-91a3-63c75a62bdf3.

Error Message

Failed to deserialize message 2 for conversation Some(ConversationId(0061bbde-d01e-4285-91a3-63c75a62bdf3)): unknown variant `dlaczego uruchamiając zsh dostaję błąd: extension: nie znaleziono polecenia . @/.zshrc `, expected `Text` or `Command`

ERROR: Failed to convert context record to domain type for conversation 0061bbde-d01e-4285-91a3-63c75a62bdf3
    Caused by: unknown variant `dlaczego uruchamiając zsh dostaję błąd: extension: nie znaleziono polecenia . @/.zshrc `, expected `Text` or `Command`

Corrupted Data

The raw_content field was stored as a plain string instead of a properly serialized EventValue enum:

{
  "text": {
    "role": "User",
    "content": "<task>dlaczego uruchamiając zsh dostaję błąd: extension: nie znaleziono polecenia . @/.zshrc </task>\n<system_date>2025-11-04</system_date>",
    "raw_content": "dlaczego uruchamiając zsh dostaję błąd: extension: nie znaleziono polecenia . @/.zshrc ",  // ← WRONG: plain string
    "model": "glm-4.6"
  }
}

Expected format:

{
  "raw_content": {"Text": "dlaczego uruchamiając zsh dostaję błąd: extension: nie znaleziono polecenia . @/.zshrc "}
}

Root Cause

The UserPrompt struct had the #[serde(transparent)] attribute, which caused EventValue::Text(UserPrompt("string")) to serialize as just "string" instead of the expected {"Text": "string"} format. This made it impossible to deserialize back to the EventValue enum.

Code Location

File: crates/forge_domain/src/event.rs:84

Original Code (BROKEN):

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, From, Deref)]
#[serde(transparent)]  // ← This caused the issue
pub struct UserPrompt(String);

Why This Happened

When UserPrompt has #[serde(transparent)], serde treats it as if the outer struct doesn't exist during serialization. This means:

  1. EventValue::Text(UserPrompt("test")) serializes to just "test" (transparent)
  2. Expected serialization: {"Text": "test"} (enum variant)
  3. When deserializing "test" back to EventValue, serde fails because it expects {"Text": ...} or {"Command": ...}

Fix

File: crates/forge_domain/src/event.rs:84

Change: Removed the #[serde(transparent)] attribute from the UserPrompt struct:

// Before (BROKEN):
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, From, Deref)]
#[serde(transparent)]
pub struct UserPrompt(String);

// After (FIXED):
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, From, Deref)]
pub struct UserPrompt(String);

Tests Added

Added two comprehensive tests to prevent regression in crates/forge_domain/src/event.rs:

Test 1: test_event_value_serialization_roundtrip

Verifies that EventValue::Text serializes to proper enum structure and can be deserialized back correctly:

#[test]
fn test_event_value_serialization_roundtrip() {
    let original = EventValue::Text(UserPrompt("test string".to_string()));
    let json = serde_json::to_string(&original).unwrap();

    // Verify JSON has proper enum structure, not just as string
    assert!(json.contains("\"Text\""), "EventValue should serialize as enum variant, not transparent string");

    let deserialized: EventValue = serde_json::from_str(&json).unwrap();
    assert_eq!(original, deserialized, "Round-trip serialization should preserve EventValue");
}

Test 2: test_event_value_command_serialization

Verifies that EventValue::Command serializes with proper variant structure:

#[test]
fn test_event_value_command_serialization() {
    let command = UserCommand::new("test_cmd", Template::new("test"), vec![]);
    let original = EventValue::Command(command);
    let json = serde_json::to_string(&original).unwrap();

    // Verify JSON has proper enum structure
    assert!(json.contains("\"Command\""), "EventValue::Command should serialize with variant name");

    let deserialized: EventValue = serde_json::from_str(&json).unwrap();
    assert_eq!(original, deserialized, "Round-trip serialization should preserve Command variant");
}

Verification

All existing tests pass:

  • 15 event tests
  • 39 conversation tests
  • 8 user_prompt tests

New serialization tests pass:

  • test_event_value_serialization_roundtrip
  • test_event_value_command_serialization

Full workspace builds successfully

The fix ensures:

  • EventValue enum variants serialize with their variant names (Text or Command)
  • No transparent serialization of inner values
  • Proper round-trip serialization/deserialization

Impact

Fixed Issues

  1. ✅ User messages with raw_content are now stored correctly in the database
  2. ✅ Existing conversations can be loaded without deserialization errors
  3. ✅ The forge conversation list command works correctly
  4. ✅ Future conversations won't have this data corruption issue

Migration Note

The corrupted conversation data currently in the database will need to be migrated or deleted. However, all new and updated conversations will store data correctly.

To fix the corrupted conversation:

# Option 1: Delete the corrupted conversation
forge conversation delete 0061bbde-d01e-4285-91a3-63c75a62bdf3

# Option 2: The conversation will be automatically fixed when you continue it (new messages will use correct serialization)
forge conversation resume 0061bbde-d01e-4285-91a3-63c75a62bdf3

Related Files

  • crates/forge_domain/src/event.rs - Fixed UserPrompt struct definition
  • crates/forge_app/src/user_prompt.rs:207 - Location where raw_content is set (uses correct EventValue)
  • crates/forge_repo/src/conversation/conversation_record.rs:797-800 - Location where deserialization error occurs

References

Metadata

Metadata

Labels

severity: highSignificant impact; core functionality is impaired.type: bugSomething isn't working.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions