Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

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

Closed Beginner friendly
#159 5 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust

Research direction

Start with the two byte-index slices in src/compact/reinject.rs: FileReinjector around lines 79-85 and SkillReinjector around lines 288-293. Add a helper truncate_on_char_boundary(s, max) that walks back with is_char_boundary, then add the four tests listed in the spec, including a CJK string over budget. Done when both reinjectors stay panic-free on multibyte content and the existing truncation marker still appears.

Written by the indexing model from the issue text.

Description

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 的默认值;不改注入内容的排版。

Dominant language
Rust
Stars
4
Forks
0
Avg merge
5h 32m
Merged PRs (30d)
7

Getting set up

  • Ships a Dockerfile or Docker Compose file
  • No pull request template
  • No contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from jeffkit/recursive

All issues in jeffkit/recursive

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.