fix(backfill): store full UIMessage in chat_messages.parts
Merged
SW
5mo ago+14+14+01234567890123456789-2-2-0123456789
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
UIMessageinchat_messages.partsโpersistLatestUserMessage.ts:42andpersistAssistantMessage.ts:27both doparts: message as never. - The read path
getSessionChatHandler.ts:44returnsmessages: rows.map(r => r.parts)and expects each to already be aUIMessage. - The backfill wrote
parts: content.partsโ just the bare parts array โ so migrated rows came back with noid/roleand 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
-
migrateRoomunit test assertspartsis the full{ id, role, parts }UIMessage. (4 tests green) -
tsc --noEmitclean on changed files;eslintclean. - Prod data reconciled + verified (counts above).
๐ค Generated with Claude Code