fix(recommend): never advertise a range the fold gate always empties (#319) - #320
Conversation
Ranges advertised as compressible could not be folded. The protected-zone carve removes individual messages after a range is built, so a residual range that brushes the zone can hold only messages whose turn would lose its reasoning run or whose call/result pair the fold would split. The fold gate withdrew all of them and the tool answered "Range would split 1 tool call/result pair(s) at the protected-zone boundary" — a range acp_status had just listed as compressible, on content that can never be folded. The gate also evaluated its two invariants in a fixed order, so withdrawing a call for the pair invariant could leave a turn whose reasoning still folded while its call stayed visible — the exact split the turn invariant forbids, and in one shape the range folded into a block holding only the turn's reasoning. Both now come from computeIntegrityWithdrawals (src/turn-integrity.ts): the two invariants (kept call keeps its reasoning run; a call/result pair never splits across the fold) are re-checked to a fixed point, and buildCompressibleRanges screens the same messages out, treating each as a gap, so every advertised range is foldable. The reverse direction — reasoning kept while the call and result fold — stays allowed (#564).
Reviewing now. I'll verify the reported failure mode against the current code, re-run the suite on this branch and on a tree merged with master, check CI, and report back here with findings. |
PR Review — #320 ✅ Recommend merge (not merged — merges are human-only here) Duplicate screening — Searched open+closed issues/PRs for the advertised-but-refused-range / fold-gate-withdrawal topic: no duplicates. Prior related work (#684 turn grouping, #564 directional allowance) is correctly referenced and preserved. No other open PR touches Diff cleanliness — Clean. Four files, every line maps to the stated purpose; no lockfile/generated churn, no mass whitespace or reformatting:
Bug verification — reproduced on current master (
Root-cause layering: the reported symptom (advertised then refused) is one face; the deeper defects are (a) recommendation evaluating integrity on a different set than the gate sees (full candidate set vs. per-range effective set after the protected-zone carve), and (b) the gate's single-pass ordering. This fix addresses both layers. Fix soundness
Verification (run locally on the branch)
CI — Required checks green: Minor observations (non-blocking)
中文摘要: 审核了 #320——修复了推荐侧把"折叠门必然清空或损坏的范围"广播出去的问题(已在当前 master 上复现:残留范围被广播,折叠后静默生成只含 reasoning 的块、留下孤儿 tool-call,两个缺陷均属实且根因判断准确);方案用共享定点函数同时约束折叠门与推荐侧,严格强于旧门、不放宽任何内核契约,diff 干净、测试充分(分支即合并后树,806/806),CI 红项是已知的 fork PR 发布问题(#288 待合,非必需检查),可以合并。 |
|
Thanks for the review and for reproducing both layers on unfixed master. Answers to the three points: The 820/820 figure. It is this branch's 806 plus the 14 tests of the companion wire codec (kernel PR #318), measured on the tree that merges the two, which is also the tree the local installation is built from. Re-measured today on a fresh merge of this branch with #318's codec onto Test-file formatting. Taken care of: The count in the "Range would split N …" message. Intentional: the number now reports actions taken until the fixed point rather than violations found in a single pass, so it is monotonically greater than or equal to the old count for the same fold. The wording paths themselves are unchanged. The residual cross-range edge you accepted is recorded as you describe it: it requires a user message between a call and its result, which the host loops in question do not emit, so folding the first range withdraws the local half at apply time with a warning rather than folding empty. Pre-existing class, unchanged by this PR. |
Independent second-agent review — verified locally, recommend merge. I reproduced both defects on unfixed master before reading this diff, so I could check the patch against my own repros rather than the issue text alone:
On PR head
Two non-blocking notes:
Bottom line: both reported defects are fixed at the root (one fixed-point verdict shared by gate and recommendation), the new tests cover the failing shapes and the no-over-withdrawal shape, and nothing else regressed. Mergeable once the artifact workflow's npm auth is sorted (or accept the red build-artifact as known infra). 中文摘要:独立复现了两个缺陷并在本分支验证修复(共享定点撤回逻辑同时用于折叠门与推荐侧,typecheck 干净、806/806 全绿);build-artifact 的 CI 失败是 npm 发布凭据缺失的环境问题而非代码问题;另发现一个极罕见的残余边界情况(跨 gap 且相距超过 maxScan=20 的 call/result 对仍可能被广告后拒绝),方向安全、不阻塞合并,建议后续按 gap 分段模拟。 |
Update to note 2 above: now reproduced empirically (sandbox disk recovered mid-review). Minimal shape on head The listing advertises Still non-blocking per the earlier reasoning; the per-gap-segment simulation suggested in note 2 closes it if you want it closed. |
|
继续 |
Problem
buildCompressibleRangesadvertises ranges that the fold gate insrc/compress.tsalways empties, soacp_statusoffers a range andcompressthen refuses it:The recommendation side filters protected messages out of the candidate set, but it evaluates turn integrity on a set the gate later sees differently: a tool call whose reasoning run, or whose result, sits inside the protected zone is advertised and then withdrawn, leaving nothing to fold.
Reviewing that path also turned up a second-order defect in the gate itself. INV1 (a kept tool call keeps its reasoning run) was evaluated before INV2 (a call and its result never split), so withdrawing a message for INV1 could leave an INV2 split standing, and one shape folded into a block holding only the tool turn's reasoning.
Fix
computeIntegrityWithdrawals(messages, foldedIds)insrc/turn-integrity.ts: re-checks both invariants to a fixed point and returns{withdrawn, splitTurnCount, splitPairCount}, so a withdrawal made for one rule cannot leave the other violated.src/compress.tscalls it where the single-pass withdrawal used to run.src/recommend.tsdemotes the same messages out of the advertised candidate set and marks each withdrawn message as a gap, so the listing only offers ranges the gate will accept.The fold-side allowance for the existing tool-pair rule (#564) is unchanged: this is a screening and fixed-point change, not a relaxation.
Tests
tests/recommend-range-integrity.test.ts: a fixture that folds once and then re-lists, withpreserveRecentMessages: 4and tool nameedit(readsits inNEVER_PRESERVE_RECENT_TOOLS, so it never forms the split the test needs). Reverting only the recommend-side filter reproduces the original failure,advertised range m00003..m00004 must fold; with the filter both cases pass, and the fold produces a block that covers the whole advertised range.Verification
npm test806/806 on this branch (11 suites), 820/820 on the tree merged with currentmaster;npm run typecheckclean. Verified against a live proxy afterwards: the previously failing session shape now folds its residue (compress requested 1 range(s)followed byCompressed … 1 block(s)), and noRange would splitline appears for any advertised range.