Skip to content

fix: prune/recommend scan history once per turn (#498) - #499

Merged
ranxianglei merged 1 commit into
masterfrom
2026-10-02_prune-recommend-rescan
Oct 3, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-10-02_prune-recommend-rescan

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #498

What

Two per-turn hotspots in large sessions, both caused by rescanning the full history:

  1. prune (src/prune.ts): rebuildMessages called pairSafeAnchorIndex for every active summary anchor; each call rebuilt the tool-result position map over ALL messages and ran a prefix scan — O(anchors × N).
  2. recommend (src/recommend.ts): isSyntheticOrPruned looped over every block's effectiveMessageIds.includes(message.id) for every candidate message, in both computeProtectedRefs and buildCompressibleRanges — O(visible × coverage).

Fix

  • prune builds one O(N) index per rebuild (buildAnchorPairingIndex: first tool-result position per call id, assistant-run starts, prefix-max of tool-call end positions); each anchor resolves by array lookups + fixed-point jumps instead of a rescan. The index is built only when there are anchors.
  • recommend precomputes the covered-id set once per helper call (coveredMessageIds) and uses Set.has. Exact-ID semantics unchanged (base-ID normalization stays exact-id membership vs block.effectiveMessageIds: remaining same-class sites (latent until sub-id projections ship) #234).
  • The synthetic-summary check in recommend now reuses SUMMARY_HEADER from prune (same string, single source).

Behavior preservation (verified)

Outputs identical to the previous implementation, verified differentially against a pre-edit snapshot of src/:

  • 3000 seeded-random prune inputs (mixed roles/content types, duplicate & unmatched tool call ids, active/inactive blocks incl. dangling ids) — output arrays deep-equal
  • 8 hand-crafted pairing edge cases (multi-step fixed point 2→5→7, duplicate call ids, unmatched calls, reasoning-run straddle)
  • 2000 × (computeProtectedRefs + buildCompressibleRanges) with config variants — deep-equal
  • Full processTurn on the issue's session shape (21,751 messages, 132 active blocks): entire output object deep-equal, including a second-pass re-feed comparison

This PR does not change the #286 tool-call/result pairing guarantee or any rendered output — it only changes how fast the same answers are computed. No new config, no new defaults, no prompt-cache byte changes.

Measurements (issue repro session, this machine, median of 16 runs)

node before after
prune 87.9ms 7.7ms
recommend 31.1ms 3.6ms

(The issue reported ~132ms/~45ms on the reporter's machine; the ratios should transfer.) Output remains 499 messages.

Tests

New tests/prune-recommend-scale.test.ts (8 tests): exact-placement pairing cases pinned against the old implementation's outputs; scale processTurn correctness (499 messages, all 132 summaries placed exactly once in order, no unanswered tool calls, second-pass stability); scale recommend correctness (covered ids never appear compressible); covered-set semantics (inactive blocks don't exclude; synthetic header does). A deliberately loose wall-time guard (<10s) keeps the suite deterministic across CI machines; the actual perf proof is the differential verification above.

Note for review

prune encodes the #286 tool-pair atomicity invariant (§7.3 item 5). This change is behavior-preserving, not a contract change — proven by the differential verification above — but it touches that module plus recommend, so it is not auto-merge eligible under §7.4 (two modules + contract-adjacent node); human review requested.

prune rebuilt the tool-result position map inside pairSafeAnchorIndex for
every active summary anchor (O(anchors x N)); recommend rescaned all blocks'
effectiveMessageIds for every candidate message (O(visible x coverage)).

Both now share one O(N) structure per pass: prune builds run-start + prefix
tool-result-end arrays once per rebuild, recommend precomputes the covered-id
set once per helper call (exact-ID semantics unchanged).

Behavior-preserving: verified byte-identical outputs vs the previous
implementation on 3000 randomized prune inputs, 2000 recommend input/config
combinations, hand-crafted pairing edge cases (multi-step fixed point,
duplicate/unmatched tool ids), and the issue's 21k-message session through
full processTurn including second-pass re-feed.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-02_prune-recommend-rescan (78df864)

Option A — Install from npm PR tag (recommended)

In your adapter project:

npm install acp-kernel@pr-499

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

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf acp-kernel-pr499.tgz
npm install ./package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到。这是带 ework-agent-pr 标记的 [PR] issue,按规范不重做,走 review 流程:拉分支核对代码、确认 CI、按结构化七问出结论。开始审查中,稍后回复完整结论。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review 完成(PR #499,base = 当前 master tip 633b8c8,单 commit,CI 全绿:test(22)/test(24)/pr-validation/build-artifact)。独立验证手段:逐行等价性分析 + 自跑 2000 例随机差分测试(base vs head,含重复/未匹配 call id、dangling block id、active/inactive 块)+ 本地定向测试(新 8 例 + 既有 prune/recommend 相关 69 例全过)+ tsc --noEmit 干净 + base/head 二遍过程对比。

结构化七问:

  1. 是什么问题:大会话每次普通请求都在重复扫描完整历史——prune 对每个摘要锚点重建一次工具结果索引并前缀扫描(O(锚点数×N)),recommend 对每条候选消息线性遍历所有块的覆盖集合做 includes()(O(可见数×覆盖量))。issue perf: prune/recommend 重复扫描历史,导致大会话普通请求增加数百毫秒延迟 #498 实测 21k 消息会话每轮 prune ~132ms / recommend ~45ms。
  2. 什么原因导致的:算法复杂度缺陷——索引本应每轮建一次却被放在每锚点/每消息循环里重复建。根因定位准确(离线 profiling 支撑),PR 修的是机制本身而非症状。
  3. 怎么修复的:prune 每次 rebuild 建一份 O(N) 索引(first-wins 工具结果位置 → 前缀最大结束位置数组 + assistant-run 起点数组),锚点用数组查表 + 定点跳板解析;recommend 复用既有 state.coveredMessageIds() 预计算 Set,Set.has 查询;合成摘要头字面量改为单一来源(复用 prune 的 SUMMARY_HEADER,已验证与旧内联字符串逐字节相同)。
  4. 是否有回归问题:未发现。逐项审过触碰路径的既有行为——① 锚点放置四语义(assistant-run 回吸、工具配对定点、重复 call id 取最早结果、越界锚点原样返回):逐行证明新旧转换函数 T(safe) 恒等,且我的独立差分测试 2000 例 deep-equal 通过;② recommend 覆盖判定:active-only 门控、exact-ID 语义(exact-id membership vs block.effectiveMessageIds: remaining same-class sites (latent until sub-id projections ship) #234 base-ID 归一化未动)均保持;③ 高危模式逐项过:无语义漂移、无 fail-fast→吞错、无阈值/时序/默认值漂移、无输出字节变化(差分含渲染后 summary 文本)、无 prompt-cache 字节变化、无管线节点顺序变化。prune/compress: a fold can split one tool call from its result (parallel burst) — unrecoverable 400 on strict OpenAI backends #286 工具配对保证由新测试的 unanswered-tool-calls 断言在规模级钉住。
  5. 是否破坏缓存:不涉及。无 state schema 变化、无 ref 复用/重发、wire artifact 不变、渲染文本字节稳定(base/head 二遍对比模式完全一致:ids 稳定、字节跨遍不恒等属既有行为,已验证 base 同样如此)。
  6. 是否改了设计方向:不涉及。纯算法优化,契约不变。§7.4 自分类正确:触及两个模块且 prune 编码 §7.3 第 5 项(工具配对原子性),虽行为保持已被差分证明,仍走人工合并门——同意。
  7. 是否引入了新的配置:不涉及。无新 env/config/默认值。

测试有效性:断言有牙——4 个精确放置用例钉死旧实现输出、规模级结构不变量(499 条输出、132 个 summary 各恰出现一次且保序、无未答工具调用、二遍稳定)、covered-set 门控语义(inactive 不排除 / 合成头排除)。wall-time 用宽松 <10s 守卫保 CI 确定性是正确取舍,性能证据在正文测量(中位数 16 次:prune 87.9→7.7ms,recommend 31.1→3.6ms)。离线 3000 例差分未入库可接受(旧实现合并后即消失,入库测试改钉语义)。

diff 整洁度:3 文件 +437/-43,全部切题,无空行/锁文件噪音,commit 规范(fix: + Fixes 引用)。

合并注意事项:open PR #490/#491 也改 prune.ts:rebuildMessages(first-user pin 区域)——三者落地顺序无论如何,后合者需 rebase 到最新 master 并重跑 typecheck+test(§7.2 stale-base 风险区);当前 mergeable_state=clean。

结论:可合(人工合并)。行为保持经独立差分验证成立,回归审查无发现,无需补修或立项。

@ranxianglei
ranxianglei merged commit cfb2de2 into master Oct 3, 2026
4 checks passed
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.

perf: prune/recommend 重复扫描历史,导致大会话普通请求增加数百毫秒延迟

1 participant