Observation-class. Nothing a user hits today; filing so it is on the record rather than rediscovered.
packages/plugin-chatbot/src/useObjectChat.ts memoizes the transport:
const transport = useMemo(() => {
if (!isApiMode) return undefined;
return new DefaultChatTransport({ api: api!, ... });
}, [isApiMode, api, headers, body, model, systemPrompt, streamingEnabled, conversationId]);
body and headers are object props, and every caller passes a fresh literal on each render. AiChatPage's ChatPane is the clearest case:
body: {
context: {
activeApp: 'AI',
agentName: activeAgent,
autoPublishAiBuilds: getRuntimeConfig().features.autoPublishAiBuilds,
...(editPackageId ? { packageId: editPackageId } : {}),
},
},
That object has a new identity on every render, so the memo never hits and a DefaultChatTransport is constructed on every render of every chat surface — during a streaming turn, that is once per token batch.
Why it is dormant rather than broken
@ai-sdk/react@4.0.59's useChat does not rebuild its Chat when the transport identity changes. It stores the latest transport in a ref and its own chatOptions.transport delegates through getTransport():
latestRef.current = { ..., transport: options.transport };
const chatOptions = { ..., transport: { sendMessages: (o) => getTransport().sendMessages(o), ... } };
const shouldRecreateChat = "chat" in options && options.chat !== chatRef.current
|| "id" in options && options.id != null && chatRef.current.id !== options.id;
useObjectChat passes neither chat nor id, so the Chat object — and with it the whole message list — survives. The wasted construction is the entire cost.
Why it is worth recording anyway
The safety here is entirely a property of the SDK's internals, not of this call site. The memo looks like it is protecting something, and the dep list reads as if a changing transport would be disruptive. An SDK upgrade that starts keying the Chat on the transport (or a future id being threaded through) would turn a silent inefficiency into a thread that resets on every render — and the memo would give a reviewer no warning, because it is already written as though it works.
Two candidate directions, neither obviously right without measuring:
- memoize at the call sites (
body / headers behind useMemo), which fixes it wherever it is done but is unenforceable and will re-rot;
- drop
body/headers from the dep list and read them through a ref inside prepareSendMessagesRequest, which is what the file already does for model (modelRef) and for the handoff id (parentConvRef) — the established idiom in this very hook, and the only one that cannot be undone by a new caller.
Found while working #2627 (the AI build thread's blank-out was a different mechanism entirely — a pane remount driven by useChatConversation clearing the conversation id; PR #4183). This was ruled out as its cause and is unrelated to it.
Observation-class. Nothing a user hits today; filing so it is on the record rather than rediscovered.
packages/plugin-chatbot/src/useObjectChat.tsmemoizes the transport:bodyandheadersare object props, and every caller passes a fresh literal on each render.AiChatPage'sChatPaneis the clearest case:That object has a new identity on every render, so the memo never hits and a
DefaultChatTransportis constructed on every render of every chat surface — during a streaming turn, that is once per token batch.Why it is dormant rather than broken
@ai-sdk/react@4.0.59'suseChatdoes not rebuild itsChatwhen the transport identity changes. It stores the latest transport in a ref and its ownchatOptions.transportdelegates throughgetTransport():useObjectChatpasses neitherchatnorid, so theChatobject — and with it the whole message list — survives. The wasted construction is the entire cost.Why it is worth recording anyway
The safety here is entirely a property of the SDK's internals, not of this call site. The memo looks like it is protecting something, and the dep list reads as if a changing transport would be disruptive. An SDK upgrade that starts keying the
Chaton the transport (or a futureidbeing threaded through) would turn a silent inefficiency into a thread that resets on every render — and the memo would give a reviewer no warning, because it is already written as though it works.Two candidate directions, neither obviously right without measuring:
body/headersbehinduseMemo), which fixes it wherever it is done but is unenforceable and will re-rot;body/headersfrom the dep list and read them through a ref insideprepareSendMessagesRequest, which is what the file already does formodel(modelRef) and for the handoff id (parentConvRef) — the established idiom in this very hook, and the only one that cannot be undone by a new caller.Found while working #2627 (the AI build thread's blank-out was a different mechanism entirely — a pane remount driven by
useChatConversationclearing the conversation id; PR #4183). This was ruled out as its cause and is unrelated to it.