文件与行号
internal/chat/stream_markdown.go:48/54(缓存判断)与 :249-255(closeOpenFences)
问题描述
缓存命中条件 strings.HasPrefix(normalized, cache.source),但 cache.source 存的是 closeOpenFences(text) 处理过的文本。当上一 chunk 停在未闭合代码 fence 内部时,closeOpenFences 给 cache.source 追加了 "\n```";下一 chunk 的 normalized 是旧文本+delta(不含这个追加的 fence),前缀匹配必然失败。
触发场景(已实测复现)
chunk1="```go\nfoo" → cache.source="```go\nfoo\n```";chunk2="```go\nfoobar" → HasPrefix("```go\nfoobar\n```", "```go\nfoo\n```") = false(第 9 字符起 foo 后要求 \n 实际是 b)。控制组(无 fence 文本)缓存正常命中。
命中失败时 reuse=0,所有 blocks 全量重渲染——不仅是增长的尾部块。c5fb20a7 引入缓存的注释自述目标是避免 "every chunk re-renders the ENTIRE growing block",而流式输出代码块(恰恰是 LLM 回复最常见长块场景)期间该目标完全失效。
预期行为 vs 实际行为
- 预期:流式期间未变块复用缓存
- 实际:代码块未闭合期间每 chunk 全量重渲染(输出仍正确,纯性能回归,长回复+块多时 UI 卡顿风险放大)
修复建议
缓存 closeOpenFences 前的原始文本用于前缀比较,渲染时再补全 fence;或对前缀比较剥离尾部补全的 fence。
严重程度
Medium(经独立 subagent 复核 + 包内测试实测 chunk1/chunk2 均缓存未命中、无 fence 控制组命中)
文件与行号
internal/chat/stream_markdown.go:48/54(缓存判断)与:249-255(closeOpenFences)问题描述
缓存命中条件
strings.HasPrefix(normalized, cache.source),但 cache.source 存的是closeOpenFences(text)处理过的文本。当上一 chunk 停在未闭合代码 fence 内部时,closeOpenFences 给 cache.source 追加了"\n```";下一 chunk 的 normalized 是旧文本+delta(不含这个追加的 fence),前缀匹配必然失败。触发场景(已实测复现)
chunk1=
"```go\nfoo"→ cache.source="```go\nfoo\n```";chunk2="```go\nfoobar"→HasPrefix("```go\nfoobar\n```", "```go\nfoo\n```")= false(第 9 字符起foo后要求\n实际是b)。控制组(无 fence 文本)缓存正常命中。命中失败时 reuse=0,所有 blocks 全量重渲染——不仅是增长的尾部块。c5fb20a7 引入缓存的注释自述目标是避免 "every chunk re-renders the ENTIRE growing block",而流式输出代码块(恰恰是 LLM 回复最常见长块场景)期间该目标完全失效。
预期行为 vs 实际行为
修复建议
缓存 closeOpenFences 前的原始文本用于前缀比较,渲染时再补全 fence;或对前缀比较剥离尾部补全的 fence。
严重程度
Medium(经独立 subagent 复核 + 包内测试实测 chunk1/chunk2 均缓存未命中、无 fence 控制组命中)