fix(compact): 压缩后重注入按字节下标切片 → UTF-8 边界 panic
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- rust
- Lĩnh vực
- ai-infra-agents
Hướng nghiên cứu
Bắt đầu với hai phép cắt chuỗi theo chỉ số byte trong src/compact/reinject.rs: FileReinjector quanh dòng 79-85 và SkillReinjector quanh dòng 288-293. Thêm hàm hỗ trợ truncate_on_char_boundary(s, max) lùi lại bằng is_char_boundary, sau đó thêm bốn kiểm thử được liệt kê trong đặc tả, bao gồm một chuỗi CJK vượt ngân sách. Hoàn thành khi cả hai reinjector đều không panic với nội dung nhiều byte và dấu hiệu cắt ngắn hiện có vẫn xuất hiện.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
优先级 P1 · 依赖:无 · 本单属 #186「上下文与压缩契约」簇(tracking 单,见该单的落地顺序与簇级验收)
基线 HEAD = 14bac15a。
Summary
src/compact/reinject.rs:79-85(FileReinjector):
let max_chars = self.per_file_budget * 4;
let truncated = if content.len() > max_chars {
format!("{}...\n[... truncated for compaction; use Read on the path for full text]", &content[..max_chars])
content 是 String,&content[..max_chars] 是字节下标切片;max_chars = 5_000 * 4 = 20_000(DEFAULT_PER_FILE_BUDGET=5_000,src/preset.rs:69/565)。
- 技能注入同型:
reinject.rs:288-293的&body[..max_chars]。 - 全目录无保护:
grep -rn "char_boundary" --include=*.rs src/compact零命中。 - 内容来自 Read 工具每次完整读入的
ReadFileState(src/tools/execution/fs.rs:356);调用点在src/runtime.rs:900与:915(跨轮压缩内),panic 发生在AgentRuntime::run内、turn 已落盘之后,运行时路径没有catch_unwind。 - Rust 语义保证:非字符边界的字节切片必 panic(
byte index 20000 is not a char boundary)。
影响
默认 standard 预设(开启 reinject_recent_files)下,agent 读过任意一个 >20KB 且含非 ASCII 的文件(本仓中文文档即是:一个汉字 3 字节,20_000 落在字符中间的概率约 2/3),此后一次跨轮压缩即 panic,终结该 turn 的任务(HTTP 请求任务 / CLI 进程)。技能形态同理(SKILL.md 正文 >20KB 且含中文)。
建议
- 两处都改为按字符边界截断:
content.floor_char_boundary(max_chars)(或自行用is_char_boundary回退),再切片。 - 顺带把
max_chars更名/改成与语义一致(当前是按「预算 token × 4」当字节用)。 - 验收:新增测试——构造一个多字节字符跨在 20_000 字节上的字符串,调用两个 reinjector,断言不 panic 且输出是合法 UTF-8 前缀。
边界
不覆盖压缩摘要/前缀注入侧的其他问题(见相邻单)。
实施规格(补充 · 2026-10-10)
目标:两处按字节下标的切片改为按字符边界,消除跨轮压缩路径上的确定性 panic。
改动清单
| 文件 | 改动 |
|---|---|
src/compact/reinject.rs:79-85(FileReinjector) |
&content[..max_chars] → 边界安全截断 |
src/compact/reinject.rs:288-293(SkillReinjector) |
&body[..max_chars] 同上 |
| 同文件顶部 | 新增 pub(crate) fn truncate_on_char_boundary(s: &str, max_bytes: usize) -> &str |
设计
/// Truncate `s` to at most `max_bytes` bytes without splitting a char.
/// MSRV 1.75:`str::floor_char_boundary` 仍是 nightly,故手写。
pub(crate) fn truncate_on_char_boundary(s: &str, max: usize) -> &str {
if max >= s.len() { return s; }
let mut end = max;
while end > 0 && !s.is_char_boundary(end) { end -= 1; }
&s[..end]
}
调用点保持既有格式串不变(省略号 + [... truncated for compaction; use Read on the path for full text])。
测试
| 用例 | 断言 |
|---|---|
truncate_stops_at_char_boundary |
"中".repeat(10) 截 5 字节 → 返回 "中"(3 字节),不 panic |
truncate_returns_full_string_when_short |
max >= len 时原样返回 |
file_reinjector_survives_cjk_over_budget |
7000 个汉字(21000 字节)经 FileReinjector::reinject → 不 panic,输出含截断标记 |
skill_reinjector_survives_cjk_over_budget |
同型,走 SkillReinjector |
陷阱
max_chars这个名字本身就是误导(它按"预算 token × 4"算,实际当字节用)。建议同 PR 更名max_bytes,并保留一句注释说明"预算 token ≈ 字节 / 4"。- 截断后的
att_len_chars(reinject.rs内用于预算累加)现阶段按truncated.len() / 4 + 1计算,保持原样即可,不要在本单改动预算语义。
不做:不改 per_file_budget / token_budget 的默认值;不改注入内容的排版。
- Ngôn ngữ chính
- Rust
- Star
- 4
- Fork
- 0
- Merge trung bình
- 5 giờ 32 phút
- Pull request đã merge (30 ngày)
- 7
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của jeffkit/recursive
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 53/100
jeffkit/recursive#195 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 15/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 15/100
Maintainer thường phản hồi trong vòng 1 ngày
-
tracking: 安全面闭环——已落地机制的绕过(4 项)Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 8/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của jeffkit/recursive
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
cmlarsen/writemorebetter#158 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
-
state:triage-needed
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
scanaislop/aislop#482 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
docs(skills): issue-first should reference an existing Linear ticket instead of filing a duplicateĐang mởdocumentation
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 74/100
Maintainer thường phản hồi trong vòng 1 ngày