Skip to content

streaming SSE: UTF-8 chunk parsing + partial-delta loss (follow-up to PR #412) #413

@appergb

Description

@appergb

PR #412 (streaming polish output) 合并时 pr-agent 二/三轮反馈两条流式 SSE 的边缘 case,按 "do not iterate" 协议没在 PR 内修复,单独跟踪。

1. UTF-8 chunk parsing 跨 frame 切分

`response.chunk().await` 返回的 byte 切片是按 HTTP frame 切的,可能在多字节 UTF-8 字符中间断开。当前 `polish.rs::chat_completion_messages_streaming`(流式润色)和 `chat_completion_history_streaming`(QA 流式)都直接 `std::str::from_utf8(&chunk)?`,命中切分时整条流误判为 "non-utf8 SSE chunk" 失败。

中文 / 日文 / emoji 输出概率 ~1/2700 per frame(8KB chunk × 3 bytes/char)。罕见但真实。

修法:bytes buffer + 增量 UTF-8 解码(`encoding_rs::Decoder::decode_to_string` 或手写)。

影响范围:同时 fix 两条路径(流式润色 + QA 流式)。

2. typer 部分 delta 丢失

`coordinator/dictation.rs::run_streaming_polish` 的 typer 任务里:

```rust
match crate::unicode_keystroke::type_unicode_chunk(&delta) {
Ok(()) => typed_text.push_str(&delta),
Err(e) => first_failure = Some(e.to_string()),
}
```

`type_unicode_chunk` 内部逐 codepoint 发送,如果在第 N 字成功、第 N+1 字失败,前 N 字已经物理落到屏幕但不会进 `typed_text`。导致 history / clipboard 比屏幕短,第一字 case 还可能触发 "zero chars typed → fallback to one-shot" 让用户屏幕上看到重复内容。

修法:`type_unicode_chunk` 返回实际成功字数(`Result<usize, TypeError>` 而非 `Result<(), TypeError>`),typer 按返回值 truncate delta 后 push。

罕见:每个 SSE delta 通常 1-5 字,partial-delta 失败只丢前缀部分,差值 ≤ delta size。

参考

PR #412 pr-agent 评论:

  • 第 3 轮:UTF-8 parsing
  • 第 4 轮:Partial chunk loss

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions