fix(backfill): store full UIMessage in chat_messages.parts

Merged
SW
5mo ago
+14-2

Summary

The chat-workflow cutover (recoupable/chat#1747) needs migrated history to read back through the workflow path. That surfaced a shape mismatch in the Phase 2 backfill:

  • Native writes store the entire UIMessage in chat_messages.parts โ€” persistLatestUserMessage.ts:42 and persistAssistantMessage.ts:27 both do parts: message as never.
  • The read path getSessionChatHandler.ts:44 returns messages: rows.map(r => r.parts) and expects each to already be a UIMessage.
  • The backfill wrote parts: content.parts โ€” just the bare parts array โ€” so migrated rows came back with no id/role and wouldn't render.

This stores { id, role, parts } to match the native shape.

Data already corrected

The ~46k already-migrated rows were fixed in place on prod via a one-time, reversible SQL reconstruction from the existing columns (the original array is preserved nested under parts):

UPDATE chat_messages
SET parts = jsonb_build_object('id', id, 'role', role, 'parts', parts)
WHERE jsonb_typeof(parts) = 'array';  -- targets only backfilled rows; native rows are objects

Post-update verification: 46,616 object rows / 0 array rows; 0 rows missing id/role/parts keys; 0 rows where inner id/role diverge from the columns.

This code change keeps the owed pre-promotion straggler re-run consistent (write-once, so it only adds new rows in the correct shape).

Test plan

  • migrateRoom unit test asserts parts is the full { id, role, parts } UIMessage. (4 tests green)
  • tsc --noEmit clean on changed files; eslint clean.
  • Prod data reconciled + verified (counts above).

๐Ÿค– Generated with Claude Code