From 2a7a6efe6f8e678b11418f86e3a5245b7de439c1 Mon Sep 17 00:00:00 2001 From: Diogenes Pelisson Date: Sat, 17 Oct 2026 23:09:46 -0300 Subject: [PATCH] fix(ai): serialize ai_messages content before insert on Postgres On Postgres `ai_messages.content_json` is `text`, and Bun SQL binds a plain array to a text parameter via String(), storing '[object Object]'. Replaying that history yields empty user content and provider calls fail (Anthropic: 400 "user messages must have non-empty content"). JSON.stringify the content at the insert. SQLite passes strings through unchanged, and reads on both dialects JSON.parse `_json` strings, so behavior there is identical. Kept narrow on purpose: stringifying every object param in the PG adapter would turn values bound to jsonb columns into JSON string scalars and break jsonField() queries. Fixes #593. Co-Authored-By: Claude Opus 5.5 --- server/ai/conversations/store.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/ai/conversations/store.ts b/server/ai/conversations/store.ts index a953a75d4..cf0bfdf93 100644 --- a/server/ai/conversations/store.ts +++ b/server/ai/conversations/store.ts @@ -432,8 +432,9 @@ export async function appendMessage( const cacheReadTokens = input.cacheReadTokens ?? 0 const cacheCreationTokens = input.cacheCreationTokens ?? 0 - // Pass content as a plain array; the DB boundary handles JSON - // encoding/decoding for `_json` columns. + // Serialize content explicitly: on Postgres `content_json` is `text`, and + // Bun binds a plain array to a text parameter via String(), storing + // '[object Object]'. Reads JSON.parse `_json` strings on both dialects. const { rows: msgRows } = await tx` insert into ai_messages ( id, conversation_id, position, role, content_json, @@ -442,7 +443,7 @@ export async function appendMessage( cache_read_tokens, cache_creation_tokens ) values ( - ${id}, ${conversationId}, ${position}, ${input.role}, ${input.content}, + ${id}, ${conversationId}, ${position}, ${input.role}, ${JSON.stringify(input.content)}, ${input.toolCallId ?? null}, ${input.toolName ?? null}, ${promptTokens}, ${completionTokens}, ${costUsd}, ${cacheReadTokens}, ${cacheCreationTokens}