Skip to content

Commit 56f7737

Browse files
committed
Remove syncMessagesColumn and legacy messages JSON column
The messages JSON column on the sessions table was kept for backward compatibility with older docker-agent versions. This is no longer needed. - Remove syncMessagesColumn, syncMessagesColumnTx, syncMessagesColumnWith methods - Remove loadMessagesFromLegacyColumn fallback - Remove sync calls from AddMessage, UpdateMessage, AddSubSession, AddSummary - Add migration 020 to DROP the messages column from the sessions table - Remove 4 backward/forward compatibility tests that validated the column Assisted-By: docker-agent
1 parent 9537c5b commit 56f7737

4 files changed

Lines changed: 7 additions & 295 deletions

File tree

pkg/session/migrations.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ func getAllMigrations() []Migration {
344344
ALTER TABLE sessions DROP COLUMN split_diff_view;
345345
`,
346346
},
347+
{
348+
ID: 20,
349+
Name: "020_drop_messages_column",
350+
Description: "Drop the legacy messages JSON column now that all data lives in session_items",
351+
UpSQL: `ALTER TABLE sessions DROP COLUMN messages`,
352+
},
347353
}
348354
}
349355

pkg/session/store.go

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -329,33 +329,6 @@ type SQLiteSessionStore struct {
329329
db *sql.DB
330330
}
331331

332-
// syncMessagesColumn rebuilds the messages JSON column from session_items for backward compatibility.
333-
// This allows older versions of docker agent to read sessions created by newer versions.
334-
func (s *SQLiteSessionStore) syncMessagesColumn(ctx context.Context, sessionID string) error {
335-
return s.syncMessagesColumnWith(ctx, s.db, sessionID)
336-
}
337-
338-
// syncMessagesColumnTx is like syncMessagesColumn but uses an existing transaction.
339-
func (s *SQLiteSessionStore) syncMessagesColumnTx(ctx context.Context, tx *sql.Tx, sessionID string) error {
340-
return s.syncMessagesColumnWith(ctx, tx, sessionID)
341-
}
342-
343-
// syncMessagesColumnWith rebuilds the messages JSON column using the provided querier.
344-
func (s *SQLiteSessionStore) syncMessagesColumnWith(ctx context.Context, q querier, sessionID string) error {
345-
items, err := s.loadSessionItemsWith(ctx, q, sessionID)
346-
if err != nil {
347-
return fmt.Errorf("loading session items: %w", err)
348-
}
349-
350-
messagesJSON, err := json.Marshal(items)
351-
if err != nil {
352-
return fmt.Errorf("marshaling messages: %w", err)
353-
}
354-
355-
_, err = q.ExecContext(ctx, "UPDATE sessions SET messages = ? WHERE id = ?", string(messagesJSON), sessionID)
356-
return err
357-
}
358-
359332
// UpdateSessionTokens updates only token/cost fields.
360333
func (s *InMemorySessionStore) UpdateSessionTokens(_ context.Context, sessionID string, inputTokens, outputTokens int64, cost float64) error {
361334
if sessionID == "" {
@@ -695,8 +668,6 @@ type sessionItemRow struct {
695668
}
696669

697670
// loadSessionItems loads all items for a session from the session_items table.
698-
// If no items exist in session_items, it falls back to the legacy messages JSON column
699-
// for backward compatibility with sessions created by older docker agent versions.
700671
func (s *SQLiteSessionStore) loadSessionItems(ctx context.Context, sessionID string) ([]Item, error) {
701672
return s.loadSessionItemsWith(ctx, s.db, sessionID)
702673
}
@@ -727,9 +698,8 @@ func (s *SQLiteSessionStore) loadSessionItemsWith(ctx context.Context, q querier
727698
}
728699
rows.Close()
729700

730-
// If no session_items found, fall back to legacy messages column
731701
if len(rawRows) == 0 {
732-
return s.loadMessagesFromLegacyColumn(ctx, sessionID)
702+
return nil, nil
733703
}
734704

735705
// Now process the collected rows, making recursive calls as needed
@@ -799,38 +769,6 @@ func (s *SQLiteSessionStore) loadSessionWith(ctx context.Context, q querier, id
799769
return sess, nil
800770
}
801771

802-
// loadMessagesFromLegacyColumn loads messages from the legacy messages JSON column.
803-
// This is used for backward compatibility with sessions created by older docker agent versions
804-
// that haven't been migrated to the session_items table yet.
805-
func (s *SQLiteSessionStore) loadMessagesFromLegacyColumn(ctx context.Context, sessionID string) ([]Item, error) {
806-
var messagesJSON sql.NullString
807-
err := s.db.QueryRowContext(ctx, "SELECT messages FROM sessions WHERE id = ?", sessionID).Scan(&messagesJSON)
808-
if err != nil {
809-
if errors.Is(err, sql.ErrNoRows) {
810-
return nil, nil
811-
}
812-
// Handle case where messages column doesn't exist (very old or corrupted database)
813-
// This can happen if the database was created before the messages column was added
814-
// or if migrations failed partially
815-
if sqliteutil.IsNoSuchColumnError(err) {
816-
slog.Warn("messages column not found in sessions table, returning empty messages", "session_id", sessionID)
817-
return nil, nil
818-
}
819-
return nil, err
820-
}
821-
822-
if !messagesJSON.Valid || messagesJSON.String == "" || messagesJSON.String == "[]" {
823-
return nil, nil
824-
}
825-
826-
var items []Item
827-
if err := json.Unmarshal([]byte(messagesJSON.String), &items); err != nil {
828-
return nil, fmt.Errorf("unmarshaling legacy messages: %w", err)
829-
}
830-
831-
return items, nil
832-
}
833-
834772
// GetSessions retrieves all root sessions (excludes sub-sessions)
835773
func (s *SQLiteSessionStore) GetSessions(ctx context.Context) ([]*Session, error) {
836774
rows, err := s.db.QueryContext(ctx,
@@ -1073,11 +1011,6 @@ func (s *SQLiteSessionStore) AddMessage(ctx context.Context, sessionID string, m
10731011
return 0, fmt.Errorf("getting last insert id: %w", err)
10741012
}
10751013

1076-
// Update messages column for backward compatibility with older docker agent versions
1077-
if err := s.syncMessagesColumn(ctx, sessionID); err != nil {
1078-
slog.Warn("[STORE] Failed to sync messages column", "session_id", sessionID, "error", err)
1079-
}
1080-
10811014
slog.Debug("[STORE] AddMessage", "session_id", sessionID, "message_id", id, "role", msg.Message.Role, "agent", msg.AgentName)
10821015
return id, nil
10831016
}
@@ -1105,15 +1038,6 @@ func (s *SQLiteSessionStore) UpdateMessage(ctx context.Context, messageID int64,
11051038
return ErrNotFound
11061039
}
11071040

1108-
// Get session ID for this message to sync the messages column
1109-
var sessionID string
1110-
err = s.db.QueryRowContext(ctx, "SELECT session_id FROM session_items WHERE id = ?", messageID).Scan(&sessionID)
1111-
if err == nil {
1112-
if syncErr := s.syncMessagesColumn(ctx, sessionID); syncErr != nil {
1113-
slog.Warn("[STORE] Failed to sync messages column", "session_id", sessionID, "error", syncErr)
1114-
}
1115-
}
1116-
11171041
return nil
11181042
}
11191043

@@ -1155,14 +1079,6 @@ func (s *SQLiteSessionStore) AddSubSession(ctx context.Context, parentSessionID
11551079
return fmt.Errorf("inserting subsession reference: %w", err)
11561080
}
11571081

1158-
// 5. Update messages column for both parent and sub-session for backward compatibility
1159-
if err := s.syncMessagesColumnTx(ctx, tx, parentSessionID); err != nil {
1160-
slog.Warn("[STORE] Failed to sync parent messages column", "session_id", parentSessionID, "error", err)
1161-
}
1162-
if err := s.syncMessagesColumnTx(ctx, tx, subSession.ID); err != nil {
1163-
slog.Warn("[STORE] Failed to sync sub-session messages column", "session_id", subSession.ID, "error", err)
1164-
}
1165-
11661082
return tx.Commit()
11671083
}
11681084

@@ -1276,11 +1192,6 @@ func (s *SQLiteSessionStore) AddSummary(ctx context.Context, sessionID, summary
12761192
return err
12771193
}
12781194

1279-
// Update messages column for backward compatibility with older docker agent versions
1280-
if syncErr := s.syncMessagesColumn(ctx, sessionID); syncErr != nil {
1281-
slog.Warn("[STORE] Failed to sync messages column", "session_id", sessionID, "error", syncErr)
1282-
}
1283-
12841195
return nil
12851196
}
12861197

pkg/session/store_test.go

Lines changed: 0 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -709,201 +709,6 @@ func TestBackupDatabase(t *testing.T) {
709709
})
710710
}
711711

712-
// TestBackwardCompatibility_ReadLegacyMessages verifies that new code can read
713-
// sessions that were created by older docker agent versions (messages in JSON column only).
714-
func TestBackwardCompatibility_ReadLegacyMessages(t *testing.T) {
715-
tempDB := filepath.Join(t.TempDir(), "test_legacy.db")
716-
717-
store, err := NewSQLiteSessionStore(tempDB)
718-
require.NoError(t, err)
719-
defer store.(*SQLiteSessionStore).Close()
720-
721-
sqliteStore := store.(*SQLiteSessionStore)
722-
723-
// Simulate a legacy session by inserting directly into the sessions table
724-
// with messages in the JSON column and NO entries in session_items
725-
legacyMessages := []Item{
726-
NewMessageItem(UserMessage("Hello from legacy")),
727-
NewMessageItem(&Message{
728-
AgentName: "test-agent",
729-
Message: chat.Message{
730-
Role: chat.MessageRoleAssistant,
731-
Content: "Hi from legacy agent!",
732-
},
733-
}),
734-
}
735-
736-
legacyMessagesJSON, err := json.Marshal(legacyMessages)
737-
require.NoError(t, err)
738-
739-
_, err = sqliteStore.db.ExecContext(t.Context(),
740-
`INSERT INTO sessions (id, messages, tools_approved, input_tokens, output_tokens, title, cost, send_user_message, max_iterations, working_dir, created_at, starred, permissions, agent_model_overrides, custom_models_used, thinking)
741-
VALUES (?, ?, 0, 0, 0, 'Legacy Session', 0, 1, 0, '', ?, 0, '', '{}', '[]', 1)`,
742-
"legacy-session", string(legacyMessagesJSON), time.Now().Format(time.RFC3339))
743-
require.NoError(t, err)
744-
745-
// Now read the session using the store API - it should fall back to messages column
746-
retrieved, err := store.GetSession(t.Context(), "legacy-session")
747-
require.NoError(t, err)
748-
require.NotNil(t, retrieved)
749-
750-
// Verify messages were read from the legacy column
751-
assert.Len(t, retrieved.Messages, 2)
752-
assert.Equal(t, "Hello from legacy", retrieved.Messages[0].Message.Message.Content)
753-
assert.Equal(t, "test-agent", retrieved.Messages[1].Message.AgentName)
754-
assert.Equal(t, "Hi from legacy agent!", retrieved.Messages[1].Message.Message.Content)
755-
}
756-
757-
// TestForwardCompatibility_MessagesColumnPopulated verifies that new code populates
758-
// the messages column so older docker agent versions can read sessions.
759-
func TestForwardCompatibility_MessagesColumnPopulated(t *testing.T) {
760-
tempDB := filepath.Join(t.TempDir(), "test_forward.db")
761-
762-
store, err := NewSQLiteSessionStore(tempDB)
763-
require.NoError(t, err)
764-
defer store.(*SQLiteSessionStore).Close()
765-
766-
sqliteStore := store.(*SQLiteSessionStore)
767-
768-
// Create a session using the new API
769-
session := &Session{
770-
ID: "new-session",
771-
CreatedAt: time.Now(),
772-
}
773-
err = store.AddSession(t.Context(), session)
774-
require.NoError(t, err)
775-
776-
// Add messages using the new granular API
777-
_, err = store.AddMessage(t.Context(), "new-session", UserMessage("Hello from new code"))
778-
require.NoError(t, err)
779-
780-
_, err = store.AddMessage(t.Context(), "new-session", &Message{
781-
AgentName: "new-agent",
782-
Message: chat.Message{
783-
Role: chat.MessageRoleAssistant,
784-
Content: "Response from new agent",
785-
},
786-
})
787-
require.NoError(t, err)
788-
789-
// Verify messages column is populated (how old docker agent would read it)
790-
var messagesJSON string
791-
err = sqliteStore.db.QueryRowContext(t.Context(),
792-
"SELECT messages FROM sessions WHERE id = ?", "new-session").Scan(&messagesJSON)
793-
require.NoError(t, err)
794-
assert.NotEmpty(t, messagesJSON)
795-
assert.NotEqual(t, "[]", messagesJSON)
796-
797-
// Parse and verify the messages column content
798-
var items []Item
799-
err = json.Unmarshal([]byte(messagesJSON), &items)
800-
require.NoError(t, err)
801-
802-
assert.Len(t, items, 2)
803-
assert.Equal(t, "Hello from new code", items[0].Message.Message.Content)
804-
assert.Equal(t, "new-agent", items[1].Message.AgentName)
805-
assert.Equal(t, "Response from new agent", items[1].Message.Message.Content)
806-
}
807-
808-
// TestForwardCompatibility_SubSessionPopulated verifies that sub-sessions
809-
// are properly serialized to the messages column for backward compatibility.
810-
func TestForwardCompatibility_SubSessionPopulated(t *testing.T) {
811-
tempDB := filepath.Join(t.TempDir(), "test_subsession.db")
812-
813-
store, err := NewSQLiteSessionStore(tempDB)
814-
require.NoError(t, err)
815-
defer store.(*SQLiteSessionStore).Close()
816-
817-
sqliteStore := store.(*SQLiteSessionStore)
818-
819-
// Create parent session
820-
parentSession := &Session{
821-
ID: "parent-session",
822-
CreatedAt: time.Now(),
823-
}
824-
err = store.AddSession(t.Context(), parentSession)
825-
require.NoError(t, err)
826-
827-
// Add a message to parent
828-
_, err = store.AddMessage(t.Context(), "parent-session", UserMessage("Start task"))
829-
require.NoError(t, err)
830-
831-
// Create and add a sub-session
832-
subSession := &Session{
833-
ID: "sub-session",
834-
CreatedAt: time.Now(),
835-
Messages: []Item{
836-
NewMessageItem(UserMessage("Sub task")),
837-
NewMessageItem(&Message{
838-
AgentName: "sub-agent",
839-
Message: chat.Message{
840-
Role: chat.MessageRoleAssistant,
841-
Content: "Sub response",
842-
},
843-
}),
844-
},
845-
}
846-
err = store.AddSubSession(t.Context(), "parent-session", subSession)
847-
require.NoError(t, err)
848-
849-
// Verify parent's messages column contains the sub-session
850-
var messagesJSON string
851-
err = sqliteStore.db.QueryRowContext(t.Context(),
852-
"SELECT messages FROM sessions WHERE id = ?", "parent-session").Scan(&messagesJSON)
853-
require.NoError(t, err)
854-
855-
var items []Item
856-
err = json.Unmarshal([]byte(messagesJSON), &items)
857-
require.NoError(t, err)
858-
859-
assert.Len(t, items, 2) // user message + subsession
860-
assert.Equal(t, "Start task", items[0].Message.Message.Content)
861-
assert.NotNil(t, items[1].SubSession)
862-
assert.Equal(t, "sub-session", items[1].SubSession.ID)
863-
assert.Len(t, items[1].SubSession.Messages, 2)
864-
}
865-
866-
// TestForwardCompatibility_SummaryPopulated verifies that summaries
867-
// are properly serialized to the messages column for backward compatibility.
868-
func TestForwardCompatibility_SummaryPopulated(t *testing.T) {
869-
tempDB := filepath.Join(t.TempDir(), "test_summary.db")
870-
871-
store, err := NewSQLiteSessionStore(tempDB)
872-
require.NoError(t, err)
873-
defer store.(*SQLiteSessionStore).Close()
874-
875-
sqliteStore := store.(*SQLiteSessionStore)
876-
877-
// Create session
878-
session := &Session{
879-
ID: "summary-session",
880-
CreatedAt: time.Now(),
881-
}
882-
err = store.AddSession(t.Context(), session)
883-
require.NoError(t, err)
884-
885-
// Add messages and a summary
886-
_, err = store.AddMessage(t.Context(), "summary-session", UserMessage("Hello"))
887-
require.NoError(t, err)
888-
889-
err = store.AddSummary(t.Context(), "summary-session", "This is a summary of the conversation.")
890-
require.NoError(t, err)
891-
892-
// Verify messages column contains the summary
893-
var messagesJSON string
894-
err = sqliteStore.db.QueryRowContext(t.Context(),
895-
"SELECT messages FROM sessions WHERE id = ?", "summary-session").Scan(&messagesJSON)
896-
require.NoError(t, err)
897-
898-
var items []Item
899-
err = json.Unmarshal([]byte(messagesJSON), &items)
900-
require.NoError(t, err)
901-
902-
assert.Len(t, items, 2)
903-
assert.Equal(t, "Hello", items[0].Message.Message.Content)
904-
assert.Equal(t, "This is a summary of the conversation.", items[1].Summary)
905-
}
906-
907712
// TestOrphanedSubsessionReference verifies that loading sessions gracefully
908713
// handles orphaned subsession references (where the subsession was deleted
909714
// but the reference in session_items remains).

pkg/sqliteutil/sqlite.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,6 @@ func IsCantOpenError(err error) bool {
5959
return false
6060
}
6161

62-
// IsNoSuchColumnError checks if the error is due to a missing column in SQLite.
63-
// This typically happens when querying a column that doesn't exist in the schema.
64-
func IsNoSuchColumnError(err error) bool {
65-
if sqliteErr, ok := errors.AsType[*sqlite.Error](err); ok {
66-
// SQLITE_ERROR (1) is the generic SQL error code used for "no such column"
67-
return sqliteErr.Code() == sqlite3.SQLITE_ERROR
68-
}
69-
return false
70-
}
71-
7262
// DiagnoseDBOpenError provides a more helpful error message when SQLite
7363
// fails to open/create a database file.
7464
func DiagnoseDBOpenError(path string, originalErr error) error {

0 commit comments

Comments
 (0)