react-langchain: every values event reconverts every message when UI lives in graph state

Author: okisdevCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbug

problem

useStreamThreadRuntime merges the uiStateKey snapshot (stream.values.ui by default) over the live custom channel fold in mergeUIMessages. upstream builds a fresh state object for every values event and only keeps identity for messages: in @langchain/langgraph-sdk 1.11.0, RootMessageProjection.applyValues reconciles messages by id, while stateValuesShallowEqual compares every other key with Object.is. an unchanged ui list therefore arrives as a new array of new entry objects on every superstep, so mergedUiMessages, uiMessagesByParent and convertWithUI all change and every cached conversion misses.

measured on main (f36e94500) through useStreamRuntime with mocked useStream and useChannel, over five rerenders that only replace values.ui with an equal copy:

  • 10 root messages and one snapshot UI entry: 50 convertLangChainBaseMessage calls, 0 of 10 message identities kept.
  • one subagent with 20 nested messages and a snapshot UI entry on one of them: 100 nested conversions and a new transcript array each time, because useSubagentTranscripts compares each subagent's UI entries by identity.

this is separate from #7416, which covers the custom channel fold: a fold that keeps entry identity still hands mergeUIMessages new snapshot entries on every values event. per parent invalidation (#7148, #7155) would narrow the root side to the messages that carry snapshot UI, but those parents and their nested transcripts would still reconvert on every superstep.

fix

recover identity where the snapshot enters the merge: when the snapshot array changes, reuse the previous entry for an id if the new entry is structurally equal to it, so an unchanged snapshot yields the previous entries and the previous merged list. the comparison runs once per values event and is proportional to the snapshot, not to the thread.

Track in Rupic

Source: assistant-ui/assistant-ui