Skip to content

fix: nudge 推荐侧与 compress 执行侧统一字符计数(#359) - #360

Open
ranxianglei wants to merge 3 commits into
masterfrom
2026-09-03_recommend-exec-counter-alignment
Open

fix: nudge 推荐侧与 compress 执行侧统一字符计数(#359)#360
ranxianglei wants to merge 3 commits into
masterfrom
2026-09-03_recommend-exec-counter-alignment

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Issue

#359(来源 #355 长会话缝隙分析;与 #37 同族)

根因(已代码级证实)

  • 推荐侧 buildCompressibleRangeslib/messages/inject/utils.ts):非 text part 用 JSON.stringify(整个 part).length / 4 —— 含 type/tool/callID/state.status 包装字段 + JSON 转义开销(内容里每个 \n" 多花 1 char)→ tool 消息系统性高估约 10–40%
  • 执行侧 lib/compress/range.ts:180-201:soft filter 后求和 countMessageCharacterslib/token-utils.ts:224-237)= text 长度 + input + output/error 正文长度
  • 后果:sub-floor 的 tool-heavy range 被 nudge 推荐、却被管线 min 检查拒绝(Range too small)→ 注定失败的重试循环(chore: bump version to 1.6.0 #37 ses_7fb5cbc8 ×10;长会话范围压缩产生'缝隙残留'孤儿消息,建议孤儿回收/碎片整理机制 #355 v1.14.26 实测 2760 chars vs min 3000,作者全程 nudge-driven)

修复

buildCompressibleRanges 两个分支(compressible + protected)统一改用 Math.round(countMessageCharacters(msg) / 4) —— 零 tokenizer 成本,推荐门槛 ≡ 执行侧接受谓词。残留差异仅 per-message 四舍五入 ±0.5 tokens/msg(≤2 chars/msg),比修复前偏差小 2–3 个数量级,测试中以 rounding band 钉住。per-part 循环仅保留分类职责(isTool/toolPct/hasMeaningfulPart/protected tool 名收集);分组、soft-filter 镜像、zone sizing、所有 display-only counter 均未动(devlog REQ non-goals)。

测试

新增 tests/recommend-exec-counter-alignment.test.ts(7 例):issue 点名的 4 类 fixture(纯文本 / 正常完成 tool / error 态含堆栈 / 深嵌套 JSON 输出)+ #355 事故形态 gate 等价性复现(exec 2866 < 3000;pre-fix 膨胀估计 780 ≥ floor 750 → 修复后 DROPPED;反事实 legacy 估计 KEPT——两侧都钉住)+ protected 分支同 counter 验证。

  • §5.7.3 反向验证:revert 修复后 7 例中 5 例失败(2 例纯文本按设计通过),恢复后全绿 —— 测试精准命中该 bug
  • 全量套件 1069/1069 ✅ · typecheck ✅ · 新文件 prettier-clean(utils.ts 存在 70 行仓库存量 drift(master 共 421 文件未格式化);已用 .prettierrc 归一化 diff 验证本 PR 零新增 drift,故不顺手重排整个文件)

Devlog

devlog/2026-09-03_recommend-exec-counter-alignment/{REQ.md,WORKLOG.md}

…359)

buildCompressibleRanges sized non-text parts with JSON.stringify(whole part)/4,
systematically overstating tool-heavy ranges (~10-40%) vs the pipeline's
countMessageCharacters. Sub-floor ranges passed the recommendation floor but
were rejected by the min-size check (Range too small), inviting guaranteed-
failed retry loops (#37 ses_7fb5cbc8; #355 incident v1.14.26: 2760 chars vs
min 3000).

Both branches now use Math.round(countMessageCharacters(msg) / 4); per-part
loops retained for classification only. Residual divergence is per-message
rounding (<=0.5 tokens/msg), asserted as a band in the regression suite.

Tests: tests/recommend-exec-counter-alignment.test.ts (7 new; verified to fail
5/7 against pre-fix code per AGENTS.md 5.7.3). Full suite 1069/1069.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

📦 Built Plugin Artifact

Branch: 2026-09-03_recommend-exec-counter-alignment (16db2a0)

Option A — Install from npm PR tag (recommended)

opencode plugin opencode-acp@pr-360 --global

Each push to this PR publishes a new version under the pr-360 npm tag.

Option B — Install from GitHub

opencode plugin "github:ranxianglei/opencode-acp#2026-09-03_recommend-exec-counter-alignment" --global

Option C — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf opencode-acp-pr360.tgz
cp -r package/dist ~/.cache/opencode/packages/opencode-acp@latest/node_modules/opencode-acp/dist
  1. Restart opencode to pick up changes.

This comment is automatically updated on each push.

- wrap merged token-utils import to satisfy printWidth (no new prettier violations)
- add compacted-tool-output fixture pinning the placeholder path (review M3)
- document incident-fixture triple constraint + safe edit window (review M2)
- reword exec-side comment to its actual scope (review N2)
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 PR #360 审核进行中 — 初审结论与证据如下,第二路代码审核完成后补最终 verdict。

1. 重复判定:非重复 ✅

2. Triage 三步结论

① 复现/查证 — 根因代码级证实。 我逐行读了两侧:

  • 执行侧 lib/compress/range.ts:180-202:soft filter 后对存活消息求和 countMessageCharacterslib/token-utils.ts:224-237),低于 minCompressRange 即抛 Range too small
  • 修复前推荐侧:非 text part 用 Math.round(JSON.stringify(整个 part).length / 4) —— 包装字段 + JSON 转义开销,tool part 系统性高估;
  • floor 接线核实:inject.ts:389resolveEffectiveFloor() = floor(minCompressRange/4)(默认 5000→1250),filterRecommendedRangeseffective >= floor 过滤 —— 即推荐门槛确实直接吃这个 counter,口径统一后「推荐 ≡ 接受」成立。
    ② 层次判断 — 用户分层正确fix: effective compressible accounting stops phantom-range retry loops #325 之后残留的正是这一层(口径分歧),不是症状层修补。
    ③ 方案评估 — 打到核心:per-message round(chars/4) 零 tokenizer 成本、两侧同函数;残留仅 per-message ±0.5 tokens(≤2 chars/msg),比修复前偏差小 2–3 个数量级。non-goals(display counter、zone sizing、不加 chars 字段)diff 中均未越界。

3. 本地独立验证(pr-360 @ f3b13e6

结果
typecheck ✅ clean
新增测试文件 ✅ 7/7
§5.7.3 反向验证(revert 修复后重跑) 5 fail / 2 pass(2 例纯文本按设计通过)—— 与 PR 声明完全一致
全量套件 1069/1069(原 1062 +7)
GitHub CI @ f3b13e6 ✅ pr-validation / build / build-artifact / test(22) / test(24) / e2e 全部 success;mergeable=True(behind master,无冲突)

4. 双 agent 审核(§5.3 + §5.6)

Reviewer 1(测试审核):APPROVE-WITH-NITS 🟢

  • 4 类 fixture 均真实命中 bug(实测 legacy 高估 3.3–8.9%:T3 4.3% / T4 6.0% / T5 4.3% / 事故 range 8.9%),legacy > effective 断言因正确原因成立,且等式断言钉死反方向;
  • 事故形态数值复核:execChars=2866(低于 3000 共 134 chars)、floor=750、修复后 effectiveTokens=716(worst-case rounding 718,距 floor 仍有 32 tokens)——确定性无 flake;
  • 静态反向验证:pre-fix 恰好 T3–T7 失败且全在 counter 断言上(非 setup 断言),T1/T2 纯文本按设计通过 —— PR 声明属实;
  • NITs:① 测试头注释「10–40%」措辞偏松(本组 fixture 实测 3.3–8.9%,方向与跨 floor 行为已正确钉住);② 事故 fixture 距 min 余量 134 chars,建议下次触碰时放宽到 ≥200;③ §5.7 严格读法:PR 改了 lib/messages/inject/utils.ts 但未新增 multi-turn injectCompressNudges @ preserveRecentMessages>0 测试 —— 现有套件覆盖完好(tests/inject.test.ts 含 prm:2 全增长周期 + baseline 断言 94 处调用),且被改函数在 nudge firing/baseline 逻辑之外,不阻塞,留档备查。

Reviewer 2(代码审核):运行中 —— 重点核查 reasoning/file/pending-tool/compacted 边界、.tokens 下游消费方、protected 分支 zone-sizing 双侧等价声明、残余风险量化。完成后本帖补最终 verdict。

5. 其他发现(NIT)

  • devlog/.../WORKLOG.md 有 prettier 表格对齐 drift(~24 行纯 padding diff),而 WORKLOG 声称「REQ/WORKLOG prettier-clean」—— 轻微不实,建议 npx prettier --write 一下或修正措辞。

当前状态:除第二路审核外全部通过,倾向 approve(human merge)。 终审结论稍后补充。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 终审:APPROVE ✅(双 agent 均 APPROVE-WITH-NITS,零 blocker / 零 major;CI 全绿)。以下为第二路代码审核结论 + 汇总。

Reviewer 2(代码审核)要点

  1. 修复正确且最小:两分支现均为 Math.round(countMessageCharacters(msg) / 4)lib/messages/inject/utils.ts:785,811),与 exec gate 在 lib/compress/range.ts:190 求和的函数完全一致。测试从真实源码导入,JSON.stringify helper 是刻意复刻 pre-fix 估计器以钉住 delta。

  2. 残余风险量化(关键问题「还会不会推荐出注定被拒的 range」):

  3. zone-sizing 不对称(NIT,正确处理)computeProtectedRefsinject/utils.ts:1154-1161)仍用旧 per-part counter,而 filterProtectedRecentMessages 位于 lib/compress/protected-content.ts:279(:304 同一 counter)——我本地 grep 复核确认两侧 counter 逐字节相同 → zone-pair 不变式精确成立;膨胀的 zone counter 只会让 preserveRecentTokens 略早饱和 → 保护区略大 → 推荐更少,方向保守(永不导致拒绝),按 REQ non-goals 正确未动。⚠️ 小勘误:devlog 引用位置写的 search.ts,实际在 protected-content.ts(纯文档笔误)。

  4. 语义漂移 benign:file/step/patch part 从「计入」变 0 —— exec 侧本就算 0(extractToolContent 对非 tool type 返回 []),对齐正是目的。全部 .tokens/.effectiveTokens 消费方核查:接受谓词 filterRecommendedRanges(inject/utils.ts:983-984,已对齐)、display-only formatCompressibleRanges(1032-1115)、nudge 文案(inject.ts:575)、acp_status(status.ts:325)——无下游逻辑依赖旧膨胀值。

  5. perf / 兼容:part 每消息遍历两次(counter pass + 分类 loop),仍 O(total parts),无新 O(n²);diff 无新 as any/@ts-ignore;签名未变;无持久化状态/API shape 变更 ✓。

汇总 nit 清单(均不阻塞,可合并后跟进)

# Nit 建议
1 WORKLOG.md prettier 表格 padding drift(~24 行纯对齐 diff),与文中「prettier-clean」表述不符 npx prettier --write devlog/2026-09-03_recommend-exec-counter-alignment/WORKLOG.md
2 devlog 引用 filterProtectedRecentMessages 位置为 search.ts,实际 lib/compress/protected-content.ts:279 一行勘误
3 测试头注释「10–40%」偏松(fixture 实测 3.3–8.9%;方向与跨 floor 行为已正确钉住) 措辞可选收紧
4 事故 fixture 距 min 余量 134 chars 下次触碰时放宽到 ≥200 chars
5 §5.7 严格读法:PR 改了 inject/utils.ts 但未新增 multi-turn injectCompressNudges @ prm>0 测试 现有套件覆盖完好且被改函数在 nudge-firing/baseline 逻辑之外,留档备查即可

最终判定

  • 本地独立验证:typecheck ✅ · 新增 7/7 ✅ · §5.7.3 反向验证 5 fail/2 pass(与 PR 声明逐字吻合)✅ · 全量 1069/1069
  • GitHub CI @ f3b13e6pr-validation / build / build-artifact / test(22) / test(24) / e2e 六项全绿;mergeable=True(behind master,无冲突,合入前 rebase 或等待均可)
  • Triage 三步 + 重复判定 + §5.3/§5.6 双 agent 审核全部完成,结论见上两层楼

PR 可合并#360 —— 按 AGENTS.md §5.1.1.2,merge 为 human-only 操作,请 owner 自行点击 Merge。nit #1/#2 若希望合并前处理,我可以补一个 follow-up commit(说一声即可)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

const tokens = Math.round(countMessageCharacters(msg) / 4)

这是写死的吗

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 是写死的字面量,但不是本 PR 引入的 —— 它是全项目既有的 chars→tokens 启发式约定,本 PR 只统一了「字符怎么数」,没动「除以几」:

1. /4 的出处(修复前就两侧一致)

  • 推荐侧旧代码同样是 /4Math.round(text.length / 4)Math.round(JSON.stringify(part).length / 4)
  • floor 侧 resolveEffectiveFloorlib/messages/inject/utils.ts:947)= Math.floor(minCompressRange / 4)
  • 全 lib/ 共 25+ 处同一约定,甚至 quality-gate 里已起了名:lib/compress/quality-gate/evaluate.ts:15 const CHARS_PER_TOKEN_ESTIMATE = 4
  • 所以本 PR 的对齐性质不依赖 /4 是否精确:minCompressRange 本身以字符为单位配置(默认 5000),推荐 gate(Σ round(cᵢ/4) ≥ floor(minChars/4))和执行 gate(Σcᵢ ≥ minChars)锚定同一个字符配置 + 同一个系数,两侧恒等成立,残留只有 per-message ±0.5 舍入(终审已量化)。

2. 为什么不用真 tokenizer
buildCompressibleRanges 在 messages.transform hook 里每次 LLM 调用都对全部消息跑一遍,用 @anthropic-ai/tokenizer 成本不可接受(项目此前专门修过 logger 同步 tokenize 慢 268× 的问题)。devlog REQ 明确把「零 tokenizer 成本」列为修复目标之一。这个估计值只需要两边一致,不需要绝对准。

3. 已知的精度取舍(既有,非本 PR)
/4 对英文 prose 大致合理;对中文(~1–1.5 char/token)和代码会系统性低估真实 token 数。这只影响 tokens 数字与真实 tokenizer 计数对比时的绝对含义(context usage 百分比等路径用的是真 tokenizer),不影响本修复的 gate 等价性。

4. 如果想收敛这个魔数,三个选项(都不属于本 PR 范围,可另开 issue):

  • (a) 抽共享常量:把 quality-gate 的 CHARS_PER_TOKEN_ESTIMATE 提升到 token-utils.ts 导出,全库引用 —— 纯重构,零行为变化,最划算;
  • (b) 配置化(如 compress.charsPerToken)—— 需要定默认值与迁移说明;
  • (c) 换真 tokenizer —— 不建议,与零 tokenizer 成本目标冲突。

需要的话我可以起一个 (a) 的小 follow-up issue/PR。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant