Skip to content

useMemo fingerprint in CopilotChat ignores object-content messages (activity/tool messages never trigger re-render) #6327

Description

@dchmurny

Description

CopilotChat (v2, packages/react-core/src/v2/components/chat/CopilotChat.tsx) memoizes the messages array it hands to its chatView slot using a fingerprint computed per message:

const contentKey = typeof m.content === "string" 
  ? m.content.length
  : Array.isArray(m.content) 
    ? m.content.length 
    : 0;

For messages whose content is a plain object (e.g. activity/progress messages shaped like { steps: [...] }), contentKey is always the constant 0 regardless of how the object's contents change. The dependency array of the useMemo therefore never changes for these messages, so CopilotChat never recomputes messages when only object-content fields mutate — downstream components (e.g. a custom chatView) receive a stale, frozen message even though the underlying agent state (via onActivitySnapshotEvent / onRunFinalized) is updating correctly.

This is distinct from #4676 (already fixed): that issue was about @ag-ui/client mutating message objects in place and MemoizedActivityMessage's shallow/reference comparator not detecting changes. That underlying fix is present and correct in 0.0.57 — but CopilotChat's own memo sits one layer above it and independently blocks the update via this fingerprinting bug.

Impact

Any custom activity/progress renderer registered via renderActivityMessages appears frozen on its first snapshot; a run's live step progress and completion state (e.g. "Finished in Xs") never render, even though the agent event stream is correct.

Reproduction

  1. Register a custom activity renderer via renderActivityMessages that emits ACTIVITY_SNAPSHOT events with replace: true and object content (e.g. { steps: [{ name: 'step1', status: 'running' }] })
  2. Emit a second ACTIVITY_SNAPSHOT with the same messageId but different content (e.g. { steps: [{ name: 'step1', status: 'done' }] })
  3. Observe: the renderer receives the updated content in agent.messages (verifiable via useEffect(() => { console.log(agent.messages) }, [agent.messages])) but the UI never re-renders

Environment

  • @copilotkit/react-core@1.64.1
  • @ag-ui/client@0.0.57
  • @ag-ui/core@0.0.57

Suggested Fix

Include a stable content-derived key for non-string/array content, e.g. JSON.stringify(m.content).length or a content hash, instead of falling back to a constant 0:

const contentKey = 
  typeof m.content === "string" 
    ? m.content.length
    : Array.isArray(m.content) 
      ? m.content.length 
      : JSON.stringify(m.content).length;

Or better yet, use the full JSON.stringify(m.content) for activity messages to ensure any content change is detected.

Workaround Used

Subscribe directly to the agent's raw event stream (agent.subscribe(...)) in a separate React Context provider mounted alongside chatView, bypassing CopilotChat's messages memo entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions