Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

fix(compact): 压缩后重注入按字节下标切片 → UTF-8 边界 panic

クローズ 初心者向け
#159 コメント 5 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 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 の本文から書いたものです。

説明

needs-human

优先级 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 且含中文)。

建议

  1. 两处都改为按字符边界截断:content.floor_char_boundary(max_chars)(或自行用 is_char_boundary 回退),再切片。
  2. 顺带把 max_chars 更名/改成与语义一致(当前是按「预算 token × 4」当字节用)。
  3. 验收:新增测试——构造一个多字节字符跨在 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 ファイルあり
  • プルリクエストのテンプレートなし
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

jeffkit/recursive のほかの issue

jeffkit/recursive の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。