fix(compact): 压缩后重注入按字节下标切片 → UTF-8 边界 panic
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 72/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- rust
調査の方向性
src/compact/reinject.rs の 2 か所のバイトインデックスによるスライスから始めてください。FileReinjector は 79-85 行目付近、SkillReinjector は 288-293 行目付近です。is_char_boundary を使って前方へ戻る処理を行うヘルパー truncate_on_char_boundary(s, max) を追加し、そのうえで仕様に挙げられた 4 つのテストを追加してください。その中には上限を超える CJK 文字列のテストも含みます。両方の reinjector がマルチバイトの内容でも panic せず、既存の切り詰めマーカーが引き続き表示されれば完了です。
索引モデルが issue の本文から書いたものです。
説明
优先级 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 的默认值;不改注入内容的排版。
- 主要言語
- Rust
- スター
- 4
- フォーク
- 0
- 平均マージ
- 5時間 32分
- マージ済み PR(30日)
- 7
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
jeffkit/recursive のほかの issue
-
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 53/100
jeffkit/recursive#195 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 15/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 15/100
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 8/100
メンテナーはふだん 1 日以内に返信
jeffkit/recursive の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
bytecodealliance/rustix#1703 ·
メンテナーはふだん 3 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
メンテナーはふだん 1 日以内に返信
-
linux build warningオープン
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
Pomax/vst-plugins#24 ·
メンテナーはふだん 1 日以内に返信
-
V1 manifest lists written by Java lose added/existing/deleted file counts対応中かも @kacper-li-airspace-intelligence が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
apache/iceberg-rust#3445 ·
メンテナーはふだん 4 日以内に返信
-
Slack integration: links on self-hosted domains never unfurl (parseCapShareUrl only accepts cap.so / cap.link)対応中かも @mangeshraut712 が今日担当しました。 オープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
CapSoftware/Cap#2546 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信