A long reasoning stream takes the whole daemon down with SIGABRT and every agent on the host is interrupted. The reply itself is modest - one 128,000-byte part - yet a standalone reproduction of the timeline store retains about 123 MiB after an explicit garbage collection, and the projection it feeds contains one row. Double the message length and the retained memory roughly quadruples. This is getpaseo/paseo#5882, fixed in PR #6262.
Why it happens
Streaming code often builds rows like this:
const suffix = message.slice(offset) // shares `message`'s backing store
timeline.push({ text: suffix }) // and now every row pins the whole message
V8 can share a string's storage with the string it was sliced from. A 64-byte suffix is not 64 bytes of heap - it is a window onto the whole accumulated message as it stood when the chunk arrived. Keep every suffix in a growing store and the retained memory sums over all of those full-length snapshots, which is where the square comes from. A 128 KB stream of 64-byte chunks is 2,000 rows, each pinning up to 128 KB.
Anything that slices per delta is exposed. The fix here went into the one path that slices on every streamed chunk. Suffixes cut once per completed part hold at most one copy and are fine.
The fix
Emit an independent copy of each suffix rather than a view onto the accumulated string, and copy in UTF-16 code units so a surrogate pair split across two chunks still arrives intact. After the change, memory grows with the length of the text, not its square.
If you are writing your own timeline or token stream display, the check is simple. Anywhere a row holds message.slice(n) where message keeps growing, that row is a snapshot of the whole message. Copy, or hold offsets and slice at render time from one live string.
Reported with the standalone reproduction by a contributor on the linked issue.