Repository navigation
fix(core,docs): correct what the deferred-tool bridge made stale or untested - #12355
Conversation
…ntested #10410 landed (`70edbf47`) and invalidated three claims that are now in main, two of them mine, plus it exempted a tool from the eager allowlist without a test. **An exemption shipped untested.** `isExemptFromEagerAllowList` gained `tool_call` — necessary, because `tool_search` reviews a withheld schema and `tool_call` invokes it, so withholding `tool_call` under a narrow allowlist would leave every demoted tool readable and uncallable. The exemption table in `permission-manager.test.ts` lists the other seven and not this one, so deleting that arm of the source left the whole suite green. Added, which also extends the paired "a whole-tool deny still wins over the exemption" case to it. **`docs/users/features/context-cost.md` was advising against the new default.** Its trap said a mid-session reveal invalidates the prompt-cache prefix and that `threshold: 0` only wins when a session never needs the deferred tools. The bridge keeps the declared list byte-stable, so discovery no longer rebuilds the prefix, and `threshold` now defaults to `0` for exactly that reason. Rewritten to say what the cost actually is — one round trip before first use — and when raising the threshold buys it back. Two smaller corrections on the same page: the exempt-family list omitted `tool_call`, and "it needs `tool_search` to stay on" is now both halves of the bridge, with the asymmetry that matters spelled out — ordinary deferred tools fall back to eager declaration when a bridge half is missing, while tools demoted by `tools.eager` do not. It also gains a baseline note, because an allowlist's saving is now only the eager-by-default schemas it withholds: the on-demand pool already left the first request, so a figure measured before #10410 attributes that saving to the wrong lever. **The plan doc's cost model is void, not pending.** Its section 6 carried a warning that #10410 *would* invalidate the arithmetic; it has, so the note now says so outright and names what replaced it, and the status row for #12029 records that both halves of that issue are settled. Refs #12028, #12029 Claude-Session: https://claude.ai/code/session_01P31kyHJf9VXo8w6h9uZetg
The handoff brief was written on 2026-09-16 against a build where `tools.toolSearch.threshold` defaulted to `10`, so the deferred pool was preloaded at session start whenever it fit in 10% of the window — which on a 1M window it always did. `v0.24.2` ships #10410, which makes discovery go through the `tool_search` → `tool_call` bridge without rewriting the declared list, and flips that default to `0`. Two of the brief's sections are void as a result: its measured baseline, and its cost model for demotion. Adds §0.1, placed before everything else and pointed at from the header: - what changed and why the on-demand path only now engages by default, which is also the honest explanation for part of the "tens of thousands on the first turn" the brief was written to attack; - a section-by-section list of what still holds — the allowlist semantics do, with `tool_call` added to the exempt families; the subagent risk does, verified today against `agent-core.ts`, which still asks for `includeDeferred: true`; - the re-baseline procedure, with the version floor that silently invalidates a run (0.24.1 and earlier measure the old behaviour), the derived expectation stated as derived, and the three things to check when the number does not move; - a risk table for what `v0.24.2` changes for existing users, with the one silent failure mode — the model not thinking to look for a withheld tool — and a one-line rollback that restores the old preload. §8 gains the table that separates the two attributions, because the whole point of re-measuring is to tell what #10410 gave away for free from what an eager allowlist is worth; measured together, the upstream saving lands on the deployment's configuration. It also gains the routing-miss question, which has no automated gate and matters more than any of the token figures. Refs #12028, #12333 Claude-Session: https://claude.ai/code/session_01P31kyHJf9VXo8w6h9uZetg
…cost.md Two clauses still claimed prefix-caching models such as DeepSeek are automatically opted out of the ToolSearch + ToolCall bridge: the rewritten bullet on when the bridge ends up unregistered, and the trap-list entry that ends with "DeepSeek models opt out of ToolSearch automatically for this reason". That opt-out was removed in #10410: packages/cli/src/config/config.ts gates purely on settings.tools?.toolSearch?.enabled === false and explicitly keeps the bridge enabled for prefix-cache-sensitive models, and the config tests assert deepseek models no longer get tool_search/tool_call denied. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmu9wrczi92
|
Both DeepSeek clauses are fixed in
Verified against the tree before editing: Left out of scope deliberately, per the stage-3 note that these are non-blocking and the author's call: the plan doc §2 and |
`3d302b36` removed the false fact — DeepSeek models are no longer opted out of ToolSearch automatically, since #10410 deleted the model-name regex. The sentence it left behind still carries the false implication: that for a prefix-caching model, keeping the declaration list identical beats keeping it small. Reaching a withheld tool is now prefix-stable, so for the deferral decision there is nothing left to invert, and a deployment that disabled ToolSearch by hand to protect a prefix is holding a reason that has expired. Prefix stability still argues against everything else that rewrites the prefix mid-session, so the bullet keeps that half rather than disappearing. Claude-Session: https://claude.ai/code/session_01P31kyHJf9VXo8w6h9uZetg
|
The DeepSeek finding is fixed — Verified against main before touching it: One thing worth separating, since the first commit stopped at the fact: the remaining sentence still said that for a prefix-caching model, keeping the declaration list identical beats keeping it small. Post-bridge that is no longer true for the deferral decision — reaching a withheld tool is prefix-stable — so a deployment that turned ToolSearch off by hand to protect a prefix is holding an expired reason, and the bullet now says so. It keeps the half that is still true: prefix stability argues against everything else that rewrites the prefix mid-session. Thanks for the citations. They turned this into a two-minute edit instead of a re-derivation. |
…ek opt-out The upgrade-risk section certified that v0.24.1...v0.24.2 held only 8 files / 6 PRs and that #10410 was the sole default-behaviour change. Re-derived from the tag range: 15 non-test files under packages/core/src/{tools,permissions}; three commits flip defaults — #10410 (deferral), #12096 (Bash comment permission verdicts, ungated), #12198 (undecided-workspace trust gating) — so the section now lists all three, gains risk rows for the two ungated flips, scopes the one-line rollback to deferral only, and notes at the re-baseline table that #12119 changed the /context derivation in the same release. Also extend the §0.1 status table to §6/§7: §6 row 7's DeepSeek automatic ToolSearch opt-out was deleted by #10410 (the only remaining kill-switch is the explicit tools.toolSearch.enabled: false at config.ts:2102-2114), §7 item 3 is closed now that #10410 shipped, and the plan doc's twin DeepSeek claim is corrected in the same pass. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuakc4l1ae
Three corrections against the repo's own authoritative text (settingsSchema tool descriptions, the client.ts incomplete-bridge warning, tool-registry.ts:1055): - Removal enumeration: dropping a non-exempt tool takes a whole-tool permissions.deny rule, a tools.disabled entry, or --exclude-tools (not permissions.deny alone); for the bridge pair, tools.toolSearch.enabled: false removes both halves at once. Bullet 4's incomplete-bridge cause list is widened to match the warning. - A withheld tool with a missing bridge half is not "at no route at all": it is not offered to the model and cannot load through the bridge, but stays registered and a direct call by name still goes through normal approval. - Removing a bridge half is not equivalent to giving up the allowlist: every ordinary deferred tool is force-declared into every request while the withheld tools lose their discovery route. - The threshold advice is scoped to ordinary deferred tools: the preload never reveals tools demoted by tools.eager. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuakc4l1ae
My first version certified that "only #10410 changed default behaviour" in `v0.24.1...v0.24.2` and put that under a heading saying it is the only thing to worry about. Review found three counterexamples in the range and I confirmed all three: - **#12096** (`9036855d`) adds `splitCommandForRules` at four evaluation sites in `permission-manager.ts` (+103/−5), gated only on tool name and whether the command contains a newline — no setting, no env. A comment-bearing command can flip deny→ask, or deny→allow under a broad `Bash(echo *)` rule. Nothing about deferral surfaces or undoes that, so the section as written would have sent an operator down a toolSearch-only ladder for a permission change. - **#12198** (`c9839a94`) flips `isTrustedFolder()`'s undecided default. - **#12034** (`4f027777`) added `MEMORY_CONTEXT_WARNING_MAX_TOKENS`, itself a default, which the sibling plan-doc row already records as live. The "8 files / 6 PRs" count did not reproduce under any filter either, and the list of six named two PRs that touch neither path. Re-derived from the tag range with the filter stated: 53 commits, three touching non-test files under `packages/core/src/{tools,permissions}/`, and a separate table for "changed a default", which is the wider set the section actually needed. The claim is narrowed to what is true — the *token*-relevant default change is #10410 — and a risk row for #12096 is added whose detection step is re-testing existing `Bash(...)` deny rules against comment-bearing commands, with a note that the one-line rollback cannot touch it. §8 item 0 gains the instrument caveat: #12119 shipped in the same release and rewrote how `/context detail` derives the built-in-tools row, so a pre-upgrade and a post-upgrade reading are not the same ruler. Both runs should record the whole `/context detail` output rather than one category number. Claude-Session: https://claude.ai/code/session_01P31kyHJf9VXo8w6h9uZetg
…wo rulers `0ed2941b` fixed the enumeration this review flagged, including the #12096 permission-verdict row. One half of the same finding is still open: §8 item 0 asks the reader to subtract a pre-upgrade `/context detail` reading from a post-upgrade one and call the difference #10410's saving, but #12119 shipped in the same release and rewrote how that row is derived. The two numbers come from different instruments, so the subtraction silently folds a measurement-method change into the attribution the item exists to separate. Asks for the whole `/context detail` output on both runs rather than the one category number, so the method difference stays recoverable afterwards, and says what the first row is worth if only a single figure survives. Claude-Session: https://claude.ai/code/session_01P31kyHJf9VXo8w6h9uZetg
R1-5, R1-6, R1-7, R1-8, R1-9. R1-1/R1-2/R1-3/R1-4 and §7 item 3 were handled in `0ed2941b`, `57b26d13` and `70c9c3f8`. **R1-5 — the #12029 cell over-closed.** #10410 changed a *default*; it added no cap. `client.ts` still sizes the budget as `floor(window × percent / 100)` with the percentage clamped to 100 — symmetric in percent, not in tokens — and the preload stays all-or-nothing. Default `0` only means it does not fire by default, and this repo's own verification brief tells a reader to raise the threshold when a session will certainly need those tools, which brings the gap straight back. Recorded as half-closed, with the reason. **R1-6 — the plan's §10 table contradicted the sibling README it is cross-linked to.** #12142 was listed as awaiting review after shipping in `v0.24.2` (`36887c49`), and #12030's first half as awaiting review after merging (`fbd6ccc2`) on an issue closed the same day. Both rows corrected, with the part that is genuinely not done said plainly: the conditional-rule mechanism landed, and nobody has moved any content onto it. **R1-7 — the plan's own baseline had no void banner** while the README marked its equivalent explicitly. Added at the top, naming which sections the numbers belong to and pointing at §0.1 for the re-baseline. It also corrects §10's "less than a tenth of that step" against the 4.2k/11.4k split this PR's own README states, and notes §3's exemption list needs `tool_call`. **R1-8 — the exempt-floor figure is stale.** 2,163 counted `task_stop` (which is `shouldDefer=true` and not resident) and omitted `tool_call` (exempt and resident since #10410), while `tool_search` has drifted 375 → 394. Restated at ~2,230 from the reviewer's probe, attributed to them rather than claimed as mine, with the reader's own `/context detail` named as the authority. **R1-9 — item 0 marked the wrong row required.** Row 1 is conditional on an environment the reader may not still have; row 2 is what §0.1 makes mandatory and what both subtractions need. Row 2 is now the required one, with the derived ~17,300 named as the fallback when row 1 is unobtainable. Claude-Session: https://claude.ai/code/session_01P31kyHJf9VXo8w6h9uZetg
|
All nine findings are now closed. R1-1/R1-2/R1-3/R1-4 and §7 item 3 in R1-5 — you are right that I over-closed it. #10410 changed a default and added no cap: R1-6 — both rows corrected against the tag range: #12142 R1-7 — banner added at the top of the plan doc, naming §1/§2/§5 as R1-8 — restated at ~2,230 with the three components that moved: R1-9 — row 2 is now the required one, with the derived ~17,300 named as the fallback when row 1 is unobtainable. Both subtractions stay computable. One note on process: two sessions of mine were editing this branch in parallel today, which is why some findings have two fixes. Where that happened I took the other commit's version and added only what it was missing, rather than layering a second rewrite on top. |
Bring the branch up to date with main so it contains the gate-file commit 1cc63cf (.github/workflows/ci.yml). Without it the required Lint & Static lane fails in ~26s at step 8 "Check lint gate freshness" and every later step is skipped, so the branch gets no CI signal at all. Merge only, no file content hand-edited. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmub6hg9sbk
|
@qwen-code /triage |
…nt basis
Three accuracy corrections to text this PR itself added, taken from the
approving review's non-blocking nits.
- `config.ts:2102-2114` named auth-type resolution and
`validateModelProvidersConfig`; the `tools.toolSearch.enabled === false`
gate is `shouldDisableToolSearch`. Both files now cite the symbol rather
than a line range, so the citation cannot drift again.
- §0.1 announced a `packages/core/src/{tools,permissions}` counting basis
and then listed six commits on a topical one. #12198 touches no file
under either directory and #12271/#12346 only tests, so the announced
filter yields three. The two bases are now stated separately.
- `70edbf47` has a single parent, so it is a squash commit, not a merge
commit.
|
Round 3 closeout — three of the five nits fixed, two deferred.
Deferred, both now disclosed under Risk & Scope:
The description now also covers Scope ledger, recorded below so the next pass does not restart at round 0. Baseline is the earliest reviewed head Prettier Unrelated to this PR's code: the sandboxed verification run 35621014837 from yesterday's |
Every stale claim this PR identified is now corrected in place instead of being voided by a remote banner that the reader has to have read first. plan doc: - §3: the exemption list gains `tool_call`, and the "bridge incomplete" fallback row states the real behaviour -- `resolveDeferredToolsForReminder` reveals ordinary deferred tools but withholds the `tools.eager`-demoted ones (still registered, direct call by name still uses normal approval) and warns once -- replacing "急,揭示全部". The exemption bullet now separates "exempt" from "resident" and lists the real removal levers. - §6: the whole-section void notice sits under `## 6. 成本模型`, which is where the top banner points; the `threshold: 0` subsection notice defers to it instead of contradicting its scope. §4's 盈亏线 reference and §8's "≤ 2 次" line carry their own markers. - §7 item 4 and §3's table cite `revealDeferredToolsReferencedInHistory` by symbol instead of the drifted `client.ts:1783-1822` range. - §10: the retracted "不到这一步的十分之一" sentence is rewritten in place to the post-#10410 attribution (~4.2k from #10410, ~11.4k left to the allowlist), keeping the 21,461 -> 4,080-5,766 baseline pair. verification README: - §2.6: heading, ¥ table, the ≤2 SLO blockquote and the "#10410(open)" closing line are all marked or corrected in place, so the section no longer contradicts §7 item 3's 已收口. - §0.1: the "still near 21,461" ladder gains the #12119 instrument rung; `MEMORY_CONTEXT_WARNING_MAX_TOKENS` is described as the warning threshold it actually is (`Math.min(ratioTokens, MAX)`, nothing truncates memoryContent) rather than a cap on loaded context, matching the plan doc, and the new post-upgrade warning gets its own risk-table row; the #12198 flip is scoped to `security.folderTrust.enabled`, which defaults false. - §2.1: "exempt" and "resident" are split, so `task_stop` / `mcp__*` / `computer_use__*` are no longer described as resident, while `task_stop` stays on the exemption list with the reason denying it is unsafe. - §8 item 0: one attribution rule with explicit precedence -- row3-row2 is the reliable one, row2-row1 only when row 1 is a real measurement, and ~17,300 is a row-2 expectation rather than a row-1 substitute. Item 1's direction is corrected: both §2.5 projections are floor + allowlisted schemas with no on-demand-pool component, and the floor rose 2,163 -> 2,230, so they read about 67 higher on >= 0.24.2, not lower. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmubr7c3ecn
…fix bullets context-cost.md: the prefix-cache bullet stated prefix stability for deferral unconditionally. Carry the exception tool-search.ts:133 documents -- a tool-set refresh may still re-declare a withheld tool when the live history contains a direct call to it -- without implying the bridge path itself reveals. (R3-7) context-token-governance README: the #12096 row's detection column audited only the deny side, which cannot surface the flip's lasting consequence. Rewrite it to lead with the allow side (re-audit the broad Bash(...) allow rule minted by an "Always allow" click on a comment-bearing command) and link the in-repo design doc that records the hazard. (R3-9) R3-5 is declined on the thread with evidence: no reachable configuration puts a tools.eager-demoted tool both outside getDeferredToolSummary() and genuinely unreachable for the session. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmubzrz80d3
Local real-environment verification: PR #12355 @
|
| Arms | PR head 1b956a82 built from source (pnpm worktree, npm run build && npm run bundle); released @qwen-code/[email protected] and @0.24.2 from npm; trial merge of the head into main c822995d (33 commits past the base da695fea) |
| Model | qwen3.8-max on DashScope (OpenAI-compatible), through a pass-through recording proxy that logs each request's tools array (sha256, names, sizes) and the provider's own usage.prompt_tokens_details.cached_tokens |
| Isolation | fresh QWEN_HOME + empty workspace per run, env -i, dedicated tmux socket for the TUI runs, per-run nonce in QWEN.md so cache hits cannot come from an earlier run's identical prompt |
| MCP | a real @modelcontextprotocol/sdk 1.30.0 stdio server with 8 tools and a call ledger |
| Host | macOS 26.6.2 arm64, Node 24.18.1 · 66 real provider requests in total |
1 · The core claim holds on a real provider: discovery no longer breaks the prefix
tools.eager: ["read_file"], threshold: 0, prompt "create marker.txt with the file-writing tool". The real model finds the withheld write_file by itself on both builds.
- PR head:
tool_search("select:write_file")→tool_call(write_file). Thetoolssha ise6f6413a…on all 5 requests (the same bytes as the sandbox run's scripted harness), and the request after discovery gets 9,216 cached tokens (≈95% of the previous prompt) in both runs. The file is written. - v0.24.1: the reveal changes the
toolsarray (66a9b78b→22f03eaa, 4 → 5 tools), and the next request gets only 1,024 / 5,120 cached tokens across two runs. The new bullet describes exactly this: "it used to, and old advice assumes it does". - The extra cost that remains is one round trip, which matches the new text.
2 · The new test row, on macOS and on the merge with current main
production isExemptFromEagerAllowList |
base test file (da695fea) |
PR test file |
|---|---|---|
| unmutated | 512/512 ✅ | 514/514 ✅ |
TOOL_CALL arm deleted |
512/512 ✅, the gap the PR describes | 1 failed: exemptions stay eagerly registered > tool_call, expected 'deferred' to be 'registered' |
TOOL_SEARCH arm deleted (positive control) |
2 failed | 2 failed |
git merge-tree with main c822995d is conflict-free. On the merged tree, packages/core/src/permissions/ passes 13 files, 1,025/1,025 (this includes main's new eager-allowlist-warning.test.ts from #12451). permission-manager.ts and its test are unchanged on main since the base.
3 · Bridge-removal seams with a real MCP server: all four behave as documented
permissions.deny, tools.disabled, --exclude-tools (each removing tool_call) and tools.toolSearch.enabled: false all have the same effect. All 8 MCP tools and the ordinary on-demand built-ins (task_stop, zoom_image) are force-declared, the tools.eager-demoted write_file stays withheld, and stderr logs bridge is incomplete (tool_call not registered) … they remain registered and direct calls by name still use normal approval: … write_file. The sandbox run proved the mechanism; this also covers the "all MCP tools" scale and the three seams it did not drive.
Findings
A · Suggestion: README §2.5 and §8 item 1 (new text) predict the wrong direction for the allowlist reading.
The new text says both expected values are "地板 + 白名单内 schema,不含按需池", that the ~4.2k #10410 removed "根本到不了这两个数里", and that on ≥ 0.24.2 the reading is "略高一点 (+67)". I measured the exact §2.2 aggressive list in the real TUI, v0.24.1 vs PR head. The reading goes down by 681, not up by 67.
/context detailshows Built-in tools going from 6.9k to 6.3k. On the first real request, the declared tools drop 15 → 13, and the provider'sprompt_tokensgo 15,912 → 15,231 (−681). The repo's estimator applied to the captured bodies gives exactly −681.- Why the prediction is off: an allowlist entry admits its whole family (
rule-parser.tstoolMatchesRuleToolName).read_fileadmitszoom_image(READ_TOOLS) andrun_shell_commandadmitsmonitor. Both are on demand by default, so v0.24.1's threshold-10 preload declared them, along with the exempttask_stop. The on-demand pool was therefore not "already excluded" under these allowlists. The +67 counts the floor change only (+148tool_call, +19tool_search, −102task_stop) and misses −467monitor, −279zoom_image. - The same mechanism explains the
task_stop 102in the old floor reading: v0.24.1's preload declared it on every request. The §2.1 correction's "本来就不常驻" is true on ≥ 0.24.2, but not of the v0.24.1 reading it corrects. - Suggested fix: in §2.5 and §8 item 1, say that on ≥ 0.24.2 the reading also drops by whatever on-demand tools the list admits by family (
zoom_imageviaread_file,monitorviarun_shell_command), so the direction depends on the list. Measured here: −681 for the aggressive list (real TUI) and about −0.8k for the conservative list (-p, 9.0k → 8.2k). Remove "方向…恰恰相反,会略高一点" and "别把'读数比 9,481/5,868 高几十'当成配置没生效".
B · Nit: raising threshold does not preload MCP tools at a fresh launch (context-cost.md bullet; README §0.1 MCP row).
context-cost.md now says to "raise the threshold … for ordinary deferred tools (MCP tools and the on-demand built-ins)". README §0.1 says "之前 MCP 工具只要塞得进窗口 10% 就被预加载,现在全在桥接后面". Section 2 of figure 3 shows what I measured. In every fresh launch I ran (v0.24.1 defaults and head threshold: 10, TUI and -p), the first request declared 0 of 8 MCP tools. Instead the model got "The following MCP tools became available after startup and are reachable through tool_search and tool_call." v0.24.1's first request carries the same notice ("… reachable via tool_search"). The MCP tools were preloaded only after /clear (8/8) or with QWEN_CODE_LEGACY_MCP_BLOCKING=1 (8/8). The cause is ordering: Config.initialize() calls llmClient.initialize() → startChat → preload (config.ts:4340) before it starts background MCP discovery (:4399). client.ts's own comment says late servers "stay deferred until the next session start". settings.md's "(bundled built-ins and MCP alike)" from #10410 has the same gap, so this is consistent with the existing doc and not a regression. It would help to add "at session start; MCP servers usually connect after it", and to drop or soften the §0.1 MCP row, since in the CLI v0.24.1 already kept MCP tools behind tool_search. I did not test the daemon's pooled-MCP path, where servers may already be connected when a session starts.
C · Nit (raised in the sandbox run, still open at this head): the new test comment describes an effect that does not occur.
The comment says withholding tool_call "would leave every demoted tool readable and uncallable". I deleted that arm from the compiled bundle (one chunk rewritten in an APFS clone of dist/) and ran the real model. The tools sha was still e6f6413a… (identical to head), tool_call(write_file) executed, and no warning was logged (figure 1, bottom row). ToolCallTool passes alwaysLoad=true (tool-call.ts, fourth boolean after the schema). The row is still worth keeping, but the sandbox run's suggested comment wording is accurate and mine is not needed.
Other claims checked
- Every commit sha, date and count in §0.1 checks out against full history:
70edbf47has one parent; v0.24.2 was published at 14:12:10Z; the range has 15 non-test files; the 6 topical PRs reduce to 3 under the directory filter; fix(cli): require explicit trust for undecided workspaces #12198 touches 21 files and none in those directories; fix(core): handle simple Bash comments in permission rules #12096 is +98/−5. - feat(core,docs): add conditional extension rules and cap context warnings #12034: with a 46 KB
QWEN.md(≈13.1k tokens) the startup warning is absent on v0.24.1 and present on v0.24.2 and head, and the context is not truncated (13.1k loaded on all three). - fix(cli): require explicit trust for undecided workspaces #12198: with
security.folderTrust.enabledunset, both versions honour an undecided workspace's settings. With it set totrue, head ignores them and prints "Approval mode overridden … not trusted", while v0.24.1 still trusts the workspace. - Rollback, §0.1 "推算约 17,300": with
threshold: 10, head declares v0.24.1's 27 default built-ins plustool_call. On the same build,threshold10 → 0 moves/contextBuilt-in tools from 15.6k to 9.1k (−6.5k of on-demand pool here). v0.24.1's default reads 15.5k, and v0.24.2 withthreshold: 10reads 15.6k (the gap is roughlytool_call's 148). So in this environment the cross-version drop on this line comes from the preload change, and fix(cli): make /context categories add up to the provider total #12119's re-derivation barely moves it. - §2.4 still holds with a real model: the main session declares 14 tools and a general-purpose subagent gets 17, including on-demand
monitor,zoom_imageandweb_fetch. Under the conservative allowlist, the subagent loses the demoted tools but keeps the family-admitted on-demand ones. /context detailshowstool_search394 andtool_call148, matching the corrected §2.1 figures.
Pre-existing, not introduced here (FYI, since §0.1/§8 ask readers to paste /context detail)
qwen -p "/context detail"prints onlyCommand executed successfully., on v0.24.1, head andmain. Thedetailsubcommand awaits the main action but drops its returned message (contextCommand.tsonmain, around line 1041)./context -dworks.- When
skillis itself demoted (for exampletools.eager: []), the Built-in tools category reads 0 while its per-item list sums to 542. The category subtracts theskillschema even whenskillis not declared. §2.2's lists keepskill, so they are not affected.
Not covered
The daemon/ACP deployment the README was written for, including pooled MCP; the Computer Use family (not registered here); #12096's comment-splitting permission flips (read, not exercised); the README's historical figures from a different deployment; Windows/Linux (the change is docs plus one test row).
中文版
本地真实环境验证:PR #12355 @ 1b956a82
结论:可以合并。没有改动生产代码,新增的测试行在 macOS 上被证明确实承重,与当前 main 试合并无冲突。 context-cost.md 的核心改写在真实 provider 上成立:桥接发现工具时声明列表保持逐字节不变,prompt 缓存前缀得以保留,摘掉桥接的一半时行为不对称。我在本 PR 新增的文字里发现一处可测量的错误(A),建议合并前修掉,改两句即可。B、C 是措辞小问题,可以放到后续。
沙箱那次(35621014837)是在容器里用脚本化模型跑的。本次覆盖了它「Not covered」里留下的部分:真模型接真实 provider(缓存命中数由 provider 报告)、真实 stdio MCP 服务器、真实交互式 TUI、以发布版 v0.24.1/v0.24.2 作前后对照臂、threshold > 0、全部四种摘桥方式、版本区间与提交号声明(完整历史),以及与今天 main 的试合并。
装置
| 对照臂 | PR head 1b956a82 源码构建(pnpm worktree,npm run build && npm run bundle);npm 上的发布版 @qwen-code/[email protected]、@0.24.2;head 与 main c822995d 的试合并(比 base da695fea 多 33 个提交) |
| 模型 | DashScope 上的 qwen3.8-max(OpenAI 兼容接口),前面挂一个透传录制代理,记录每个请求的 tools 数组(sha256、名字、大小)以及 provider 自己的 usage.prompt_tokens_details.cached_tokens |
| 隔离 | 每次运行新建 QWEN_HOME + 空工作区,env -i;TUI 用独立 tmux socket;QWEN.md 里放每次不同的 nonce,避免缓存命中来自之前某次运行的相同提示 |
| MCP | 真实的 @modelcontextprotocol/sdk 1.30.0 stdio 服务器,8 个工具,带调用台账 |
| 主机 | macOS 26.6.2 arm64,Node 24.18.1;共 66 次真实 provider 请求 |
1 · 核心论断在真实 provider 上成立:发现工具不再打断前缀
配置 tools.eager: ["read_file"]、threshold: 0,提示词是「用写文件工具创建 marker.txt」。两个构建上真模型都自己找到了被扣住的 write_file(见图 1)。
- PR head:
tool_search("select:write_file")→tool_call(write_file)。5 次请求的toolssha 都是e6f6413a…(与沙箱脚本化装置的字节一致);发现之后的那次请求两轮都拿到 9,216 个缓存 token(约为上一请求的 95%)。文件已写出。 - v0.24.1: 揭示工具会改写
tools数组(66a9b78b→22f03eaa,4 → 5 个工具),下一次请求两轮分别只命中 1,024 / 5,120。这正是新文字说的「以前会,旧建议仍按会来写」。 - 剩下的代价只有多一次往返,与新文字一致。
2 · 新测试行:macOS 上与当前 main 合并后
生产代码 isExemptFromEagerAllowList |
base 测试文件(da695fea) |
PR 测试文件 |
|---|---|---|
| 未变异 | 512/512 ✅ | 514/514 ✅ |
删除 TOOL_CALL 分支 |
512/512 ✅,即 PR 所说的缺口 | 1 失败:exemptions stay eagerly registered > tool_call,expected 'deferred' to be 'registered' |
删除 TOOL_SEARCH 分支(阳性对照) |
2 失败 | 2 失败 |
与 main c822995d 的 git merge-tree 无冲突。合并树上 packages/core/src/permissions/ 13 个文件 1,025/1,025 全过(包含 main 上 #12451 新增的 eager-allowlist-warning.test.ts)。自 base 以来,main 上的 permission-manager.ts 及其测试都没有改动。
3 · 接真实 MCP 服务器的摘桥方式:四种都与文档一致
permissions.deny、tools.disabled、--exclude-tools(都是摘掉 tool_call)和 tools.toolSearch.enabled: false 效果相同:8 个 MCP 工具和普通按需内置工具(task_stop、zoom_image)被强制声明;被 tools.eager 降级的 write_file 仍然扣留;stderr 打出 bridge is incomplete (tool_call not registered) … they remain registered and direct calls by name still use normal approval: … write_file(见图 3)。沙箱那次证明了机制;本次还覆盖了「全部 MCP 工具」这一规模,以及它没跑的另外三种方式。
发现
A · 建议:README §2.5 与 §8 第 1 条(新增文字)预测的读数变化方向错了。
新文字说两个预期值都是「地板 + 白名单内 schema,不含按需池」,说 #10410 省掉的约 4.2k「根本到不了这两个数里」,并说在 ≥ 0.24.2 上读数会「略高一点(+67)」。我在真实 TUI 里用 §2.2 的激进版白名单对比了 v0.24.1 与 PR head:读数下降 681,而不是上升 67(见图 2)。
/context detail的 Built-in tools 从 6.9k 降到 6.3k。首个真实请求里,声明的工具数 15 → 13,provider 报告的prompt_tokens为 15,912 → 15,231(−681)。用仓库自带估算器对抓到的请求体计算,也恰好是 −681。- 预测出错的原因:白名单条目会放行整个工具族(
rule-parser.ts的toolMatchesRuleToolName)。read_file放行zoom_image(READ_TOOLS),run_shell_command放行monitor。这两个默认都是按需工具,所以 v0.24.1 的 threshold 10 预加载把它们声明了出来,连同豁免的task_stop。也就是说,在这两份白名单下按需池并没有「本来就被排除」。+67 只算了地板的变化(tool_call+148、tool_search+19、task_stop−102),漏掉了monitor−467、zoom_image−279。 - 同一机制也解释了旧地板读数里的
task_stop 102:v0.24.1 的预加载让它出现在每一次请求里。§2.1 更正里「本来就不常驻」的说法在 ≥ 0.24.2 上成立,但不适用于它所更正的那份 v0.24.1 读数。 - 建议修法:在 §2.5 和 §8 第 1 条写明,≥ 0.24.2 上读数还会减掉白名单按族放行的按需工具(
read_file带出的zoom_image、run_shell_command带出的monitor),所以变化方向取决于白名单。本机实测:激进版 −681(真实 TUI),保守版约 −0.8k(-p,9.0k → 8.2k)。删掉「方向…恰恰相反,会略高一点」和「别把'读数比 9,481/5,868 高几十'当成配置没生效」。
B · 小问题:全新启动时,抬高 threshold 并不会预加载 MCP 工具(context-cost.md 的条目;README §0.1 的 MCP 那一行)。
context-cost.md 新写的是「为 普通 延迟工具(MCP 工具和按需内置工具)抬高 threshold」,README §0.1 写的是「之前 MCP 工具只要塞得进窗口 10% 就被预加载,现在全在桥接后面」。实测见图 3 第 2 部分:我跑过的每次全新启动(v0.24.1 默认配置、head threshold: 10,TUI 和 -p 都有),首个请求声明的 MCP 工具都是 0/8,模型拿到的是「The following MCP tools became available after startup and are reachable through tool_search and tool_call」。v0.24.1 的首个请求里也是同样的提示(「… reachable via tool_search」)。只有 /clear 之后(8/8)或设置 QWEN_CODE_LEGACY_MCP_BLOCKING=1(8/8)时才会预加载。原因在顺序:Config.initialize() 先调用 llmClient.initialize() → startChat → 预加载(config.ts:4340),之后才启动后台 MCP 发现(:4399)。client.ts 自己的注释也写着,晚连上的服务器「stay deferred until the next session start」。#10410 带进 settings.md 的「(bundled built-ins and MCP alike)」有同样的缺口,所以本 PR 与现有文档一致,不算回退。建议补一句「在会话开始时;MCP 服务器通常在那之后才连上」,并删除或弱化 §0.1 的 MCP 行,因为在 CLI 里 v0.24.1 本来就把 MCP 工具放在 tool_search 后面。daemon 的池化 MCP 路径我没有测,那里会话开始时服务器可能已经连上。
C · 小问题(沙箱那次已提出,此 head 上仍未处理):新测试注释描述的后果并不会发生。
注释说扣住 tool_call「would leave every demoted tool readable and uncallable」。我在编译产物里删掉了这一分支(在 dist/ 的 APFS 克隆里只改写了一个 chunk),再用真模型跑:tools sha 仍是 e6f6413a…(与 head 完全相同),tool_call(write_file) 照常执行,也没有任何告警(图 1 最后一行)。原因是 ToolCallTool 传了 alwaysLoad=true(tool-call.ts,schema 之后的第 4 个布尔参数)。这行测试仍值得保留;沙箱那次建议的注释措辞是准确的,不需要我另写。
其它核对过的声明(见图 4)
- §0.1 里的每个提交号、日期和计数都与完整历史吻合:
70edbf47只有一个父提交;v0.24.2 发布于 14:12:10Z;区间内有 15 个非测试文件;6 个按主题列出的 PR 在目录口径下只剩 3 个;fix(cli): require explicit trust for undecided workspaces #12198 改了 21 个文件,没有一个落在这两个目录下;fix(core): handle simple Bash comments in permission rules #12096 是 +98/−5。 - feat(core,docs): add conditional extension rules and cap context warnings #12034: 用 46 KB 的
QWEN.md(约 13.1k token),v0.24.1 不出现启动告警,v0.24.2 和 head 出现,且上下文没有被截断(三个版本都加载了 13.1k)。 - fix(cli): require explicit trust for undecided workspaces #12198: 不设
security.folderTrust.enabled时,两个版本都会采用未决工作区的设置。设为true后,head 忽略这些设置并提示「Approval mode overridden … not trusted」,v0.24.1 则仍然信任该工作区。 - 一行回滚、§0.1 的「推算约 17,300」: 设
threshold: 10时,head 声明的是 v0.24.1 默认的 27 个内置工具再加tool_call。同一构建上把threshold从 10 调到 0,/context的 Built-in tools 从 15.6k 降到 9.1k(本机的按需池为 −6.5k)。v0.24.1 默认读数是 15.5k,v0.24.2 设threshold: 10是 15.6k(差距约等于tool_call的 148)。所以在本环境里,这一行的跨版本下降来自预加载的变化,fix(cli): make /context categories add up to the provider total #12119 的口径改写对它几乎没有影响。 - §2.4 在真模型下仍成立: 主会话声明 14 个工具,general-purpose 子 agent 拿到 17 个,包括按需的
monitor、zoom_image、web_fetch。在保守版白名单下,子 agent 会失去被降级的工具,但保留按族放行的按需工具。 /context detail显示tool_search394、tool_call148,与 §2.1 更正后的数字一致。
既有问题,非本 PR 引入(供参考,因为 §0.1/§8 要读者贴 /context detail)
qwen -p "/context detail"只输出Command executed successfully.,v0.24.1、head 和main都一样。detail子命令 await 了主 action,却丢掉了它返回的消息(main上的contextCommand.ts,约第 1041 行)。/context -d可以正常输出。- 当
skill本身被降级时(例如tools.eager: []),Built-in tools 这一类显示 0,而它下面的逐项列表合计为 542。原因是这一类的总数会减去skill的 schema,即使skill并没有被声明。§2.2 的白名单保留了skill,所以不受影响。
未覆盖
README 所针对的 daemon/ACP 部署(包括池化 MCP);Computer Use 工具族(本环境未注册);#12096 注释切分带来的权限翻转(只读了代码,没有实际跑);README 里来自另一个部署的历史数字;Windows/Linux(本 PR 只改了文档和一行测试)。
|
@qwen-code /triage |
Applies the doc-accuracy findings two independent verifiers raised at 1b956a8 (wenshao's local real-environment run and the /triage sandbox round). Every one of them is in text this PR writes; no production code is touched. - README §2.5 and §8 item 1 predicted the /context allowlist reading would go up by ~67 on >= 0.24.2. Measured in the real TUI it goes down 681 for the aggressive list (~-0.8k for the conservative one), because an allowlist entry admits its whole family (toolMatchesRuleToolName): read_file brings zoom_image, run_shell_command brings monitor, and both are on-demand tools that v0.24.1's threshold-10 preload declared. The +67 floor term stays, now labelled as one of two opposing forces; the two sentences asserting the direction are removed. §2.1's correction also notes that "本来就不常驻" holds on >= 0.24.2 but not for the v0.24.1 reading it corrects. - README §0.1 MCP row and context-cost.md: the preload runs at session start and MCP servers usually connect after it, so a fresh launch declares no MCP tools at any threshold (v0.24.1 measured 0/8). - permission-manager.test.ts: the tool_call comment claimed withholding it would leave every demoted tool readable and uncallable. Both bridge halves are alwaysLoad=true, so the declaration list does not move without this arm; what it protects is the permission-deferred state other readers consult (speculation, workflow-authoring skill). The test row itself is unchanged. - #12032 rows: 1,104 characters / ~276 tokens is already stale on the main this PR merges into (prompts.test.ts there pins 1,421 / ~355), so both rows are corrected here instead of deferred. - Plan doc §3 table: four re-emitted line ranges no longer point at what they name (permission-manager.ts:848-866 now lands on isExemptFromEagerAllowList itself), so they are cited by symbol, per the convention the same table already uses for its other rows. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmucrn2bbei
|
Doc-accuracy pass — all six findings fixed, pushed as @wenshao A/B/C from your local run plus the sandbox round's Findings 2/3/4. Every one was re-verified against the tree at A · fixed as you prescribed (README §2.5 + §8 item 1). The offending text was there verbatim: §2.5 said 「这两个预期值都是
§8 item 1 carries the same correction plus your −467/−279 split; it keeps B · minimal wording only, no restructuring. C · comment reworded, test row unchanged. Applied the sandbox's suggested wording verbatim, since you judged it accurate. The two consumers it names were checked at this head rather than assumed: Sandbox Finding 2 · fixed. Verified independently: this branch's own Sandbox Finding 3 · body fixed, doc untouched. The diff does rewrite §10 in place and the banner does say 「已就地改写」, so the body was the wrong side of the contradiction. Both Risk & Scope bullets now state that §10 is rewritten in place and that no §10 follow-up is left to file. Sandbox Finding 4 · fixed by adopting the table's own symbol convention. Re-verified all four against Local verification. Approval. This push voided @wenshao's approve under the repo's stale-approval dismissal — expected, since A was asked for before merging. Review re-requested. |
qqqys
left a comment
There was a problem hiding this comment.
Critical-only review at 1df0ecfd, base da695fea.
Verdict: APPROVE — no Critical. The one blocking issue ever filed here was a documentation certification, and the text that replaces it now checks out against the code at this head. There is no production code change in this PR at all.
Scope, established first because it bounds everything else
Four files: three documentation files and seven added lines in one test file. packages/core/src production code is untouched, so there is no runtime surface here that could regress. That is worth stating plainly given the title says fix(core,…) — the core half is a test row, not a behaviour change.
The test addition pins behaviour that exists, and makes it deletable-proof
The new row is ['tool_call', 'tool_call'] in the exempt table that it.each feeds into the existing assertion. I confirmed the behaviour it pins is real rather than aspirational: PermissionManager.isExemptFromEagerAllowList at permission-manager.ts:846 includes canonicalName === ToolNames.TOOL_CALL at line 851, alongside tool_search. So the row asserts what the code does.
The comment's claim is the part that justifies the addition, and it is falsifiable in the right direction: tool_call has been exempt since the bridge landed but had no test, so deleting line 851 previously left the whole suite green. After this change it does not. The comment is also accurate about why the exemption matters even though both bridge halves are alwaysLoad = true and the declaration list therefore does not move without it — what the arm protects is the permission-deferred state other readers consult. That is a coverage addition with a stated regression it now catches, which is the useful shape.
The historical Critical: the certification no longer certifies what it cannot support
The blocking finding from the first automated round was that the upgrade-risk triage sentence in docs/verification/context-token-governance/README.md certified a v0.24.1...v0.24.2 review that had not actually been performed to that standard. The section at this head is structured the opposite way, and I verified its load-bearing claims against the code rather than reading them as prose:
- The two counting methodologies are disclosed as different rather than reconciled silently. The text states the directory-scoped count (
packages/core/src/{tools,permissions}, non-test) gives 15 files, that the thematic list gives 6 PRs, and that applying the first method naively yields 3 rather than 6 — then names exactly which PRs fall out and why: #12198's 21 files include none in those two directories, and #12271 and #12346 touch only test files there. A reader can reproduce or refute that. - Estimates are labelled as estimates. The 17,300 figure carries "这是推算,不是实测,你这次的读数就是新的事实", and the daemon pooled-MCP preload path is explicitly marked 未实测.
- The #12034 correction is accurate. I checked it:
MEMORY_CONTEXT_WARNING_MAX_TOKENS = 10_000is declared atconfig.ts:362and its only computational use isthresholdTokens = Math.min(ratioTokens, MEMORY_CONTEXT_WARNING_MAX_TOKENS)at 4977-4980. The enclosingbuildMemoryContextWarningreturnsundefinedbelow the threshold and a warning string above it;memoryContentis used only to deriveestimatedTokensand is never sliced or truncated anywhere in that path. So the doc's insistence that this is a warning threshold and not a cap on loaded context is correct, and its derived crossover point is right too — the absolute bound overtakes the 15% ratio above roughly 67k tokens,10_000 / 0.15. - The #12198 qualification is accurate.
isTrustedFolder()ends inreturn this.trustedFolder ?? !this.folderTrust(line 10122 region) andthis.folderTrust = params.folderTrust ?? false(line 3339). With the switch off,!falseistrue, so an undecided workspace still resolves as trusted and the tightening bites only where folder trust is explicitly enabled — which is exactly what the doc says and what the risk table's row repeats. - The rollback caveat is correct and is the most useful line in the section. The one-line
tools.toolSearch.threshold: 10rollback is explicitly scoped to the lazy-loading default only, with #12096 named as having no switch at all, #12198's switch identified as a different setting, and #12034's warning noted as unaffected. A reader who applied that line expecting to undo everything would be told otherwise beforehand.
I also scanned all three docs' added lines for commands that could cause harm if copy-pasted — no destructive shell, no piped remote scripts, no force-push or permission changes. The only executable blocks are qwen --version, /context detail, and the JSONC settings snippet.
Review state and CI
The latest automated round recorded zero Criticals and zero Suggestions, and no review thread is unresolved. The two CHANGES_REQUESTED reviews from 2026-09-20 are against superseded heads and are not counted in the current decision state, which reads REVIEW_REQUIRED.
Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox) and both Desktop Shell lanes pass at this head — the Test lane is the one that executes the new it.each row. review-pr and web-shell E2E Smoke are still pending; I did not wait on them and neither bears on a docs-and-test change. Nothing is failing.
|
Review details: Test +7: tool_call added to exempt array; isExemptFromEagerAllowList verified at head. context-cost.md: bridge + fallback docs verified against resolveDeferredToolsForReminder. Plans doc + Verification README: v0.24.2 corrections, §6 obsolescence notice. No blocking findings. |
|
发布记账(本 PR 已于 2026-09-22 15:45Z 合并为 已给本 PR 补上
标题不再改:squash 提交信息已经定死在 main 上,改 PR 标题不会影响它。 另一件同类的事已经发生,且还没解决:#10410( 机制与本 PR 正文预警的相同,只是判定点不是 现在给 #10410 补标签已经没有作用:v0.24.2 的 body 早已发布,且该 PR 不会再落进后续版本的提交区间。唯一的补救是编辑已发布的 v0.24.2 release body 加一行披露,再跑 |




What this PR does
Corrects what #10410 (
70edbf47, merged 2026-09-20) made stale or left untested, in one change rather than four.An exemption shipped untested.
isExemptFromEagerAllowListgainedtool_call. That is necessary —tool_searchreviews a withheld schema andtool_callinvokes it, so withholdingtool_callunder a narrow allowlist would leave every demoted tool readable and uncallable, the inverse of what the allowlist is for. But the exemption table inpermission-manager.test.tslists the other seven and not this one, so deleting that arm of the source leaves the whole suite green. This adds it, which also extends the paired "a whole-tool deny rule still wins over the exemption" case to it.docs/users/features/context-cost.mdwas advising readers against the new default. Its trap bullet said a mid-session reveal invalidates the prompt-cache prefix, and thatthreshold: 0only wins when a session genuinely never needs the deferred tools. The bridge keeps the declared tool list byte-stable, so discovery no longer rebuilds the prefix, andtools.toolSearch.thresholdnow defaults to0for exactly that reason. Rewritten to state the cost that actually remains — one extra round trip before a withheld tool's first use — and when raising the threshold buys it back.Two smaller corrections on the same page: the exempt-family list omitted
tool_call, and "it needstool_searchto stay on" is now both halves of the bridge, with the asymmetry spelled out — ordinary deferred tools fall back to eager declaration when a bridge half is missing, while tools demoted bytools.eagerdo not. The page also gains a baseline note, because an allowlist's saving is now only the eager-by-default schemas it withholds: the on-demand pool has already left the first request, so a figure measured before #10410 credits that saving to the wrong lever.The plan doc's cost model is void, not pending. Section 6 of
docs/plans/2026-09-16-non-conversation-context-token-governance.mdcarried a warning that #10410 would invalidate its arithmetic. It has, so the note says so outright, names what replaced it, and the status row for #12029 records that issue as half settled: the always-on context warning cap landed with #12034, while the preload budget still has no absolute cap whenthreshold > 0, so that half stays open. The same status table also moves #12142 (pending → merged,36887c49) and #12030 (first item pending → closed,fbd6ccc2) to theirv0.24.2state, and §3's mechanism table and exempt list are corrected in place (line ranges → symbol names,tool_call/computer_use__*added).The handoff doc gains a §0.1 that re-baselines it for
v0.24.2.docs/verification/context-token-governance/README.md(+112/−17) is the largest change in this diff, and the one the description above had been leaving out. §0.1 states what #10410 changed and its two consequences — on-demand loading now takes effect by default, and §2.6's cost model is void in full rather than merely old — then tables which of the doc's own sections still hold and which must be re-measured on ≥0.24.2: §1's baseline is void, §2.1–2.5 hold but the exempt family gainedtool_call, §2.4 is still the only real risk, §6's DeepSeek row is corrected in place. It replaces re-baselining steps 0–1 and splits the attribution that makes the old baseline void instead of stale — the ~4.2k #10410 gives away for free versus the ~11.4k an allowlist actually earns. One honest note on scope: the upgrade-risk rows §0.1 adds for #12096 (annotated-Bash permission flips) and #12198 (the pending-workspace trust gate) are about other PRs and other subsystems, so they sit past the goal stated in this description's first line. They stay because anyone re-baselining on ≥0.24.2 needs to know their measurement instrument changed twice in one release, and neither change has a switch to turn it off; splitting them into a separate change is not worth the round trip.A follow-up commit corrects three details in text this PR itself added: the
tools.toolSearch.enabled === falsegate is now cited by symbol (shouldDisableToolSearch) instead of by a line range that had already drifted ~40 lines, §0.1's commit list no longer implies the same counting basis as the file count beside it (#12198 touches no file underpackages/core/src/{tools,permissions}and #12271/#12346 only tests there, so that filter yields 3, not 6), and70edbf47is called a squash commit, which its single parent makes it.Why it's needed
The docs page is the one place that tells an operator how to cut resident context, and until this lands it tells them to avoid the default the product now ships. The untested exemption is the same class of problem one layer down: a guarantee the bridge depends on, with nothing holding it in place.
Reviewer Test Plan
How to verify
npx vitest run packages/core/src/permissions/permission-manager.test.ts— the new row passes. To see it bite, deletecanonicalName === ToolNames.TOOL_CALL ||fromisExemptFromEagerAllowListinpackages/core/src/permissions/permission-manager.ts: before this PR the suite stays green, after it thetool_callexemption case fails.docs/users/features/context-cost.mdagainstdocs/users/configuration/settings.md'stools.toolSearch.thresholdrow, which feat(core): preserve prompt cache for deferred tools #10410 already updated. The two should now agree on the default, on which tools are exempt, and on what a withheld tool costs to reach.Evidence (Before & After)
Documentation and one test row; no user-visible behaviour. The two claims removed from the page were true before #10410 and are false after it:
70edbf47threshold: 0wins only if the session never needs them"0is the default, because deferral no longer costs a prefix rebuildtool_callis exempt tooTested on
No test suite was run locally on any platform. On Linux I ran ESLint and Prettier over the changed files, and reproduced
getToolRegistrationStatus's logic for the two new cases statically (eagerTools: ['ReadFile']→tool_callregistered;permissions.deny: ['tool_call']→ disabled). CI is the arbiter. For the follow-up docs-only commit, Prettier--checkpasses on both changed files (run on macOS), and the three corrected facts were re-verified against the tree —shouldDisableToolSearchis inpackages/cli/src/config/config.ts,70edbf47has a single parent, and #12198's 21 files include none underpackages/core/src/{tools,permissions}. No source changed, so no suite was re-run.Environment (optional)
N/A — unit test and docs only.
Risk & Scope
settings.md's post-feat(core): preserve prompt cache for deferred tools #10410 description of the same setting, so the two can be compared directly.~276 tokenfigure in the feat(core): assemble the tool-policy and example sections of the system prompt from the resident tool set #12032 rows is not merely superseded on some futuremain— themainthis PR merges into already carries it (HEAD^1733f58b6hasprompts.test.tstitledsaves about 1.4k characters of policy text for a file-work allowlist, commented1,421 characters (~355 tokens) with the monitor bullet gated too, band>900/<1_500), so deferring would have landed the stale number; both rows (plan-doc §10, README §0.1) now read 1,421 字符 ≈ 355 token and say which part the old 1,104 covered. And plan-doc §10's stale "less than a tenth of that step" sentence is rewritten in place by this diff — the top banner this body pointed at as the alternative itself says 「已就地改写」 — so there is no §10 follow-up left to file.feat(core):without the!marker, sotools.toolSearch.thresholdchanging from10to0will not appear under Breaking Changes in the generatedCHANGELOG.md—scripts/generate-changelog.jsderives that flag from the conventional-commit!alone. It needs a line in the Release body by hand.Linked Issues
Refs #12028. Closes nothing on its own; records #12029 as half settled in the plan doc — the always-on warning cap landed with #12034, the preload cap is still open — which is the issue's own step to close.
中文说明
本 PR 做了什么
把 #10410(
70edbf47,2026-09-20 合入)造成的过期内容与遗漏测试一次性修掉,而不是拆成四个改动。一条豁免是没有测试就上线的。
isExemptFromEagerAllowList新增了tool_call。这是必要的——tool_search负责查看被扣住的 schema、tool_call负责调用它,所以在窄白名单下扣住tool_call,会让所有被降级的工具"看得到却调不了",恰好与白名单的目的相反。但permission-manager.test.ts的豁免表里列了另外七个、独缺这一个,因此把源码里那一支删掉,整个套件依然全绿。本 PR 补上它,同时也让配套的"整工具 deny 仍然胜过豁免"用例覆盖到它。docs/users/features/context-cost.md在劝读者不要用产品现在的默认值。 它那条坑写着"会话中途揭示工具会作废前缀缓存"、以及"threshold: 0只有在会话确实用不到那些工具时才划算"。而桥接让声明列表保持字节稳定,发现工具不再重建前缀,tools.toolSearch.threshold正是因此改为默认0。已重写为真正剩下的代价——被扣住的工具首次使用前多一次往返——以及什么时候值得抬高门限把这次往返买回来。同一页还有两处小修正:豁免族清单漏了
tool_call;"需要tool_search保持开启"现在是桥接的两半,并写清了那个关键的不对称——桥接缺一半时,普通延迟工具会回退为急加载,而被tools.eager降级的工具不会。该页还新增一条基线说明:白名单省下的现在只是它扣住的"默认急加载"那部分 schema,按需池本来就已经不在首轮请求里了;用 #10410 之前测的数字会把这笔节省记到错误的杠杆上。计划文档的成本模型是"已作废",不是"待重写"。
docs/plans/2026-09-16-non-conversation-context-token-governance.md第 6 节此前挂的是"#10410 将会让这笔账失效"。它已经失效了,所以告示改为直接说明,并点明被什么取代;#12029 的状态行也改为一半收口(常驻上下文告警上限随 #12034 合入;预加载预算在threshold > 0时仍无绝对上限,这一半仍开放)。同一张状态表还把 #12142(待审 → 已合,36887c49)与 #12030(第 1 项待审 → 已关闭,fbd6ccc2)更新为随v0.24.2发布的状态,§3 的机制表与豁免清单也就地更正(行号区间 → 符号名,补上tool_call/computer_use__*)。交接文档新增了 §0.1,把它按
v0.24.2重取基线。docs/verification/context-token-governance/README.md(+112/−17)是本 diff 里最大的一处改动,也是上面那段描述此前一直没提的一处。§0.1 先说明 #10410 改了什么、带来哪两个后果——按需加载现在默认真的生效,以及 §2.6 的成本模型是整节作废而不只是过时——随后用表格列出这份文档自己的各节哪些仍成立、哪些必须在 ≥0.24.2 上重测:§1 的基线作废,§2.1–2.5 仍成立但豁免族多了tool_call,§2.4 仍是唯一的真风险,§6 的 DeepSeek 那一行就地更正。它替换了重取基线的第 0–1 步,并把"旧基线为何是作废而不只是过期"的归因拆开:#10410 白送的约 4.2k,与白名单真正挣来的约 11.4k。关于范围有一句实话:§0.1 为 #12096(带注释 Bash 命令的权限翻转)与 #12198(未决工作区的信任门控)新增的升级风险行,讲的是别的 PR、别的子系统,因此超出了本描述第一句所说的目标。之所以留下,是因为任何要在 ≥0.24.2 上重取基线的人都需要知道自己的量具在同一个版本里变了两次,而这两处改动都没有开关可关;把它们拆成单独一个改动不值得多走一轮。后续一个提交更正了本 PR 自己写进去的三处细节:
tools.toolSearch.enabled === false这个门控现在按符号名(shouldDisableToolSearch)引用,而不再按一个已经漂移了约 40 行的行号区间;§0.1 的提交清单不再暗示与它旁边的文件计数使用同一套口径(#12198 在packages/core/src/{tools,permissions}下一个文件都没碰,#12271 与 #12346 在这两个目录下只碰测试,因此按那套过滤数是 3 个而不是 6 个);70edbf47改称为 squash 提交——它只有一个父提交,本来就是。为什么需要
那一页是唯一告诉运维者"怎么削减常驻上下文"的地方,而在本 PR 合入之前,它在劝人避开产品现在出厂的默认值。那条没有测试的豁免是同一类问题,只是低一层:桥接所依赖的一个保证,却没有任何东西钉住它。
评审测试计划
如何验证
npx vitest run packages/core/src/permissions/permission-manager.test.ts—— 新增那一行通过。想看它是否真的会咬人:把packages/core/src/permissions/permission-manager.ts里isExemptFromEagerAllowList的canonicalName === ToolNames.TOOL_CALL ||删掉;本 PR 之前整套仍绿,之后tool_call豁免那条会失败。docs/users/features/context-cost.md与docs/users/configuration/settings.md里tools.toolSearch.threshold那一行(feat(core): preserve prompt cache for deferred tools #10410 已更新)对读。两者现在应当在默认值、哪些工具豁免、以及"到达一个被扣住的工具要付什么代价"上完全一致。证据(前后对比)
文档加一行测试,无用户可见行为。被删掉的两条说法在 #10410 之前为真、之后为假:
70edbf47之后threshold: 0只有在会话用不到它们时才划算"0就是默认值,因为延迟加载不再需要重建前缀tool_call同样豁免测试平台
任何平台都没有在本机跑测试套件。在 Linux 上跑过:对改动文件的 ESLint 与 Prettier,以及对
getToolRegistrationStatus逻辑在两个新用例上的静态复现(eagerTools: ['ReadFile']→tool_call为 registered;permissions.deny: ['tool_call']→ disabled)。测试本身由 CI 裁定。后续那个只改文档的提交:两个改动文件的 Prettier--check均通过(在 macOS 上跑),且三处被更正的事实都对着代码树重新核过——shouldDisableToolSearch确在packages/cli/src/config/config.ts,70edbf47只有一个父提交,#12198 的 21 个文件里没有一个在packages/core/src/{tools,permissions}下。没有碰任何源码,因此没有重跑套件。运行环境(可选)
N/A —— 仅单测与文档。
风险与范围
settings.md中同一设置的 feat(core): preserve prompt cache for deferred tools #10410 之后描述写的,因此两者可以直接对读核验。main才会过期",本 PR 要合进去的那个main已经带着新数(HEAD^1733f58b6上prompts.test.ts的标题是saves about 1.4k characters of policy text for a file-work allowlist、注释写1,421 characters (~355 tokens) with the monitor bullet gated too、区间>900/<1_500),推迟就等于把过期数字合进去;现在两行(方案文档 §10、交接文档 §0.1)都改为 1,421 字符 ≈ 355 token,并写明旧的 1,104 只覆盖哪一部分。另外计划文档 §10 结尾那句过期的「不到这一步的十分之一」在本 diff 里已经就地改写——正文原先指向的那个顶部告示自己写的就是「已就地改写」——所以 §10 没有留下任何后续项。feat(core):(没有!)合入的,因此tools.toolSearch.threshold从10变成0不会出现在自动生成的CHANGELOG.md的 Breaking Changes 里——scripts/generate-changelog.js只从 conventional commit 的!推导这个标记。需要在 Release 正文里手写一行。关联 issue
Refs #12028。自身不关闭任何单;在计划文档中把 #12029 记为一半收口(告警上限已随 #12034 合入,预加载上限仍开放),关闭该单是 issue 自己的步骤。