544169a41a
Every tool.start / tool.progress / tool.complete ran three whole-timeline passes that each allocated per part: `completeOpenStreamParts` (`map` with a spread per row), the pending-row scan (`map` → `filter` → `map`, one wrapper object per part), and `generatedImageEchoSources` (`flatMap` + a throwaway array per non-image row) — then `dedupeGeneratedImageEchoesInParts` copied the array again even when it stripped nothing, handing React a fresh identity per event. Replace them with single in-place loops, and return the input array from the dedupe when there are no echoes so a no-op stays a no-op for the store. Measured on the real `useMessageStream` hook (vitest/jsdom, 2,000 starts + 2,000 completions interleaved with text deltas): 618 ms → 138 ms; the tool-only 2,000×2 case 252 ms → 67 ms. The remaining O(n) is one `findIndex` per event over a ~4k array (~8 µs), well under the cost of the copy React needs anyway, so no per-stream id index is introduced. Behaviour is unchanged: same first-match `toolCallId` resolution, same sparse/no-id contextual correlation, same ordering of parts. Co-authored-by: Xipong <217837358+Xipong@users.noreply.github.com>