Repository navigation
fix(#2886): make recent-zone protection pair-aware (prevent orphaned function_call) - #2892
Conversation
📦 Built Package ArtifactBranch: Option A — Install this exact build (recommended)npm install -g billion-context@0.0.3608-pr.2892.42113ddThis exact version is immutable and reproducible. The rolling Option B — Download artifact
tar xzf billion-context-pr2892.tgz
npm install -g packageInstall instructions are refreshed on each push. |
…2886) Root package-lock.json still recorded acp-kernel 0.0.119 after the #2886 bump to 0.0.120 — the lock-sync gate failed on that one-line drift (#2892 review). Also syncs the two version fields in kernel/package-lock.json, which had been stale at 0.0.114 since before this PR (pre-existing); no other lockfile content touched.
[bot] 🏷 Review 结论:方向、实现、测试均正确;发现并直接修复一处 CI 机械问题(lockfile 漂移),等重跑全绿即可合。 做了什么:拉取分支通读全 diff( 本地验证证据(worktree @
代码审查要点:
发现并直接修复的小问题(小问题直接修):CI 结构化八问:
行为变更披露(owner policy):旧 = very-recent 大工具 result 可在 call 尚在区内时被立即折叠( 判定:可合——待 |
[bot] 🏷 CI 更新:head
最终判定:代码审查结论不变(方向/实现/测试正确,详见上一条回复的结构化八问),CI 全绿, |
ccdbaf8 to
f0ad689
Compare
computeProtectedRefs can protect a recent tool-call while excluding its paired tool-result (whose name matches neverPreserveRecentTools) from the zone, since that list drops big-result tools from the visible set. The advisory carve then keeps the call visible and folds the result, stranding an orphan function_call that strict Responses validators reject with HTTP 400 invalid_request_error. Make the recent zone symmetric per pair: if either half of a (tool-call, tool-result) matched by toolCallId is protected, protect both. Monotonic - only adds refs.
f0ad689 to
5eb1adb
Compare
📦 Merged Build PublishedThis merge to master was published as npm install -g billion-context@0.0.409-master.42e5b53 # this exact build
npm install -g billion-context@master # always the newest merged build
|
kernel-change: #2886 —
computeProtectedRefsbuilds an asymmetric soft-protected zone: it can protect a recent tool-call while excluding its paired tool-result from the zone, becauseneverPreserveRecentToolsdrops big-result tools (read/bash/decompress/search_context) out of thevisibleset that the recent-N/token window scans. The advisory carve then keeps the call visible and folds the result, stranding an orphanfunction_callthat strict OpenAI Responses validators reject withHTTP 400 invalid_request_error. The fix must live in the kernel because the recent-zone computation exists only incomputeProtectedRefs; the host hookconfig.isMessageProtected(msg)receives only the message (no zone membership), so it cannot express pair-awareness.What
Make the recent-protected zone symmetric per
(tool-call, tool-result)pair: if either half (matched bytoolCallId) lands in the protected set, protect both. The pass is monotonic — it only adds refs, never removes any — so no previously-protected set can shrink. It binds bytoolCallId, not by tool name, so it holds even when the result lost its backfilledtoolName.Consumers fixed at once (single producer):
compress.ts:1059→buildCompressibleRanges) — what the plugin-mode agent is told is compressible vs protected;compress.ts:1388).Why the host side cannot solve it
The asymmetry is produced inside
computeProtectedRefs(kernel/src/recommend.ts). A host-side remedy would need to know which messages are in the recent zone in order to co-protect a counterpart — butisMessageProtected(msg)has no such context, andisMessageProtectedWithPairingcovers hardprotectedToolspairing only, not the soft recent zone. Fixing the single source corrects both consumer paths simultaneously.Behavior change (flagged per owner policy)
read/bash/decompress/search_contextresult whose call sits inside the recent zone could be folded immediately — the intended reclaimability thatneverPreserveRecentToolsprovides.preserveRecentMessages=3/preserveRecentTokens=5000). Pairs that have aged out of the zone are unchanged (still fully compressible).Deliberately NOT done
No wire sanitizer that drops orphan
function_calls incoreToResponses. Silently dropping a tool call would lose user-intent data (a wire-fidelity violation — the #1039 anti-pattern). The pair-aware zone plus the existing turn-integrity gate already prevent the orphan.Why the existing integrity gate does not suffice
The turn-integrity gate (
kernel/src/turn-integrity.ts:138-212) already auto-withdraws to keep a linked(call, result)pair intact when a fold would split it — it is a genuine backstop on the proxy-side apply path (verified: ~4000-trial fuzz through the gated path yields zero surviving orphans). It does not suffice for this bug because it is a proxy-side, apply-time cleanup, not the component that decides what the agent folds:buildCompressibleRanges←computeProtectedRefs), which was internally inconsistent: result = compressible while its paired call = recent-protected (the reporter's log lineExcluded 1 protected message(s) m00051 …(recent/last-user zone)is bili computing exactly that asymmetric set). An agent told "result compressible, call protected" will attempt a fold that strands the pair.applyCompression(and gets cleaned up) or lands in the host's own context (src/stream.ts:697: "plugin-mode agents own their folds, so bili never sees those originals") decides whether the backstop ever sees it. In this report it did not.Fixing the single producer makes the primary signal consistent and is the only lever bili has over an agent-owned fold; the gate stays as defense-in-depth for the proxy path.
Tests
kernel/tests/issue2886-recent-pair-integrity.test.ts(5 tests). Tests 1–3 fail without the fix (verified by stashingrecommend.tsand re-running); all pass with it. Test 4 guards against over-protection; test 5 pins the end-to-end "no stranded orphan at the boundary" invariant.never-preserve-recent-tools,loop-responses-orphan,tool-pair-order,wire-contract,preflight-compress-responses,protected-tools): 53/53 pass; bili typecheck + build clean. Full global bili suite left to CI.kernel/package.jsonbumped0.0.119→0.0.120(Kernel Boundary, CI-enforced). No change to thebillion-contextpackage version.Closes #2886