fix(assistant): SQL approval buttons do nothing (#48102)

Fixes FE-3954: clicking Skip/Run Query in the AI Assistant did nothing.

## Root cause
`onFinish` synced the AI SDK `Chat` instance's live message array
directly into valtio state (`chat.messages = messages`). valtio's
`proxy()` mutates an object's nested properties in place instead of
cloning them, so this corrupted the SDK's own array with Proxies. The
next approval click hit the SDK's internal `structuredClone()` call and
threw `DOMException: Proxy object could not be cloned` — an unhandled
rejection before any network request, so the buttons silently did
nothing.

## Fix
Assign a sanitized copy of the message array instead of the SDK's live
reference.

## Test plan
- [x] `pnpm --filter studio exec vitest run
state/ai-assistant-state.test.ts` — fails on old code with the exact
DOMException, passes on the fix
- [ ] Manual: approve/skip a suggested query in AI Assistant and confirm
it runs/is skipped

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved AI assistant chat message synchronization to prevent message
corruption.
* Ensured chat messages remain safely cloneable after approval-related
updates.

* **Tests**
* Added coverage verifying that synchronized AI assistant messages can
be cloned successfully.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ali Waseem authored and GitHub committed 2026-07-20 16:25:42 +00:00
1 parent 3d07613c2b
commit 4864031466
2 files changed
+49 -2

No files matched your search

@@ -0,0 +1,46 @@
import { proxy, ref } from 'valtio/vanilla'
import { describe, expect, it } from 'vitest'
import { sanitizeForCloning } from './ai-assistant-state'
describe('AI assistant chat message sync', () => {
// FE-3954: syncing the live array into valtio corrupted it with Proxies, breaking structuredClone in addToolApprovalResponse
it('does not corrupt the live SDK message array when synced into valtio state', () => {
const state = proxy<{ chats: Record<string, any>; chatInstances: Record<string, any> }>({
chats: {},
chatInstances: {},
})
state.chats['chat-1'] = { id: 'chat-1', messages: [] }
const liveSdkMessages = [
{
id: 'message-1',
role: 'assistant',
parts: [
{ type: 'text', text: "Sure, here's a query" },
{
type: 'tool-execute_sql',
toolCallId: 'tool-1',
state: 'approval-requested',
approval: { id: 'approval-1' },
},
],
},
]
state.chatInstances['chat-1'] = ref({ messages: liveSdkMessages })
const chat = state.chats['chat-1']
chat.messages = liveSdkMessages.map((message) => sanitizeForCloning(message))
// mirrors addToolApprovalResponse's own update logic
const lastMessage = liveSdkMessages[liveSdkMessages.length - 1]
const updatedParts = lastMessage.parts.map((part: any) =>
part.state === 'approval-requested' && part.approval?.id === 'approval-1'
? { ...part, state: 'approval-responded', approval: { id: 'approval-1', approved: true } }
: part
)
const replacedMessage = { ...lastMessage, parts: updatedParts }
expect(() => structuredClone(replacedMessage)).not.toThrow()
})
})
+3 -2
View File
@@ -152,7 +152,7 @@ async function clearStorage(): Promise<void> {
// Helper function to sanitize objects to ensure they're cloneable
// Issue due to addToolResult
function sanitizeForCloning(obj: any): any {
export function sanitizeForCloning(obj: any): any {
if (obj === null || obj === undefined) return obj
if (typeof obj !== 'object') return obj
return JSON.parse(JSON.stringify(obj))
@@ -329,7 +329,8 @@ function createChatInstance(
const messages = chatInstance.messages
const chat = state.chats[options.id]
if (chat) {
chat.messages = messages
// Clone first — valtio's proxy() mutates nested properties in place and would corrupt the SDK's live array
chat.messages = messages.map((message) => sanitizeForCloning(message))
chat.updatedAt = new Date()
}