Skip to content

fix(core,docs): correct what the deferred-tool bridge made stale or untested - #12355

Merged
wenshao merged 14 commits into
mainfrom
fix/post-bridge-token-docs
Sep 22, 2026
Merged

wenshao merged 14 commits into
mainfrom
fix/post-bridge-token-docs

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

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. isExemptFromEagerAllowList gained tool_call. That is necessary — 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 inverse of what the allowlist is for. But the exemption table in permission-manager.test.ts lists 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.md was advising readers against the new default. Its trap bullet said a mid-session reveal invalidates the prompt-cache prefix, and that threshold: 0 only 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, and tools.toolSearch.threshold now defaults to 0 for 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 needs tool_search to 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 by tools.eager do 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.md carried 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 when threshold > 0, so that half stays open. The same status table also moves #12142 (pending → merged, 36887c49) and #12030 (first item pending → closed, fbd6ccc2) to their v0.24.2 state, 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 gained tool_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 === false gate 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 under packages/core/src/{tools,permissions} and #12271/#12346 only tests there, so that filter yields 3, not 6), and 70edbf47 is 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

  1. npx vitest run packages/core/src/permissions/permission-manager.test.ts — the new row passes. To see it bite, delete canonicalName === ToolNames.TOOL_CALL || from isExemptFromEagerAllowList in packages/core/src/permissions/permission-manager.ts: before this PR the suite stays green, after it the tool_call exemption case fails.
  2. Read docs/users/features/context-cost.md against docs/users/configuration/settings.md's tools.toolSearch.threshold row, 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.
  3. No behaviour changes here, so nothing to exercise in a session.

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:

Claim on the page Status after 70edbf47
"a deferred tool revealed mid-session invalidates the prefix cache" false — the bridge leaves the declared list byte-stable
"threshold: 0 wins only if the session never needs them" false — 0 is the default, because deferral no longer costs a prefix rebuild
exempt families: seven listed incomplete — tool_call is exempt too

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ⚠️

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_call registered; permissions.deny: ['tool_call'] → disabled). CI is the arbiter. For the follow-up docs-only commit, Prettier --check passes on both changed files (run on macOS), and the three corrected facts were re-verified against the tree — shouldDisableToolSearch is in packages/cli/src/config/config.ts, 70edbf47 has a single parent, and #12198's 21 files include none under packages/core/src/{tools,permissions}. No source changed, so no suite was re-run.

Environment (optional)

N/A — unit test and docs only.

Risk & Scope

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 合入之前,它在劝人避开产品现在出厂的默认值。那条没有测试的豁免是同一类问题,只是低一层:桥接所依赖的一个保证,却没有任何东西钉住它。

评审测试计划

如何验证

  1. 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 豁免那条会失败。
  2. 把 docs/users/features/context-cost.md 与 docs/users/configuration/settings.md 里 tools.toolSearch.threshold 那一行(feat(core): preserve prompt cache for deferred tools #10410 已更新)对读。两者现在应当在默认值、哪些工具豁免、以及"到达一个被扣住的工具要付什么代价"上完全一致。
  3. 没有行为改动,会话里没有需要操作的部分。

证据(前后对比)

文档加一行测试,无用户可见行为。被删掉的两条说法在 #10410 之前为真、之后为假:

页面上的说法 70edbf47 之后
"会话中途揭示的延迟工具会作废前缀缓存" 假 —— 桥接让声明列表保持字节稳定
"threshold: 0 只有在会话用不到它们时才划算" 假 —— 0 就是默认值,因为延迟加载不再需要重建前缀
豁免族:列了七个 不完整 —— tool_call 同样豁免

测试平台

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ⚠️

任何平台都没有在本机跑测试套件。在 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 —— 仅单测与文档。

风险与范围

关联 issue

Refs #12028。自身不关闭任何单;在计划文档中把 #12029 记为一半收口(告警上限已随 #12034 合入,预加载上限仍开放),关闭该单是 issue 自己的步骤。

…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
yiliang114 and others added 2 commits September 20, 2026 23:44
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
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Both DeepSeek clauses are fixed in 3d302b36cb (rebased onto the concurrent README re-baseline, disjoint files).

  • The rewritten bridge bullet no longer lists "the automatic opt-out for prefix-caching models such as DeepSeek" as a way the bridge ends up unregistered.
  • The trap-list entry no longer ends with "DeepSeek models opt out of ToolSearch automatically for this reason".

Verified against the tree before editing: packages/cli/src/config/config.ts gates purely on settings.tools?.toolSearch?.enabled === false (its only effect is the two mergedDeny.push calls for tool_search/tool_call), its own comment states the bridge stays enabled "including for prefix-cache-sensitive models", config.test.ts asserts deepseek models are not denied, and a repo-wide grep finds no model-based opt-out left in the CLI/core config paths — consistent with what #10410 shipped.

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 docs/verification/context-token-governance/README.md §2.6 wording questions. prettier --check passes on the changed file.

`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
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The DeepSeek finding is fixed — 3d302b36 (another session of mine, which removed the false fact) plus d5c22c20, which removes the false implication it left behind.

Verified against main before touching it: git grep -in deepseek origin/main -- packages/cli/src/config packages/core/src/config packages/core/src/tools returns only config.test.ts assertions that the bridge stays enabled for those models, and packages/cli/src/config/config.ts:2106 gates solely on settings.tools?.toolSearch?.enabled === false. Both clauses came from a September reading of the model-name regex #10410 deleted.

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.

yiliang114 and others added 5 commits September 21, 2026 10:35
…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
@yiliang114

Copy link
Copy Markdown
Collaborator Author

All nine findings are now closed. R1-1/R1-2/R1-3/R1-4 and §7 item 3 in 0ed2941b, 57b26d13, 70c9c3f8; the remaining five in 6d7c29d8.

R1-5 — you are right that I over-closed it. #10410 changed a default and added no cap: client.ts still sizes the budget as floor(window × percent / 100) with the percentage clamped to 100, which is symmetric in percent and not in tokens, and the preload stays all-or-nothing. The cell now records it as half-closed, and names the reason the open half matters — this repo's own brief tells a reader to raise the threshold when a session will certainly need those tools, which brings the gap straight back.

R1-6 — both rows corrected against the tag range: #12142 36887c49 shipped in v0.24.2, #12030's first half merged as fbd6ccc2 and the issue closed the same day. The row now also says what is genuinely undone, which the old wording buried: the conditional-rule mechanism landed and nobody has moved content onto it, so the 10,400 tokens are still on the table.

R1-7 — banner added at the top of the plan doc, naming §1/§2/§5 as v0.24.1 readings and pointing at §0.1. It also fixes §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 — restated at ~2,230 with the three components that moved: tool_call 148 added, task_stop 102 dropped (it is shouldDefer=true, so it was never resident), tool_search 375 → 394. I did not re-run your probe, so the numbers are attributed to it rather than presented as mine, and the reader's own /context detail is named as the authority. Thank you for dropping the two limbs that did not survive verification — that is the part that made this a correction rather than a re-argument.

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
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@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.
@yiliang114
yiliang114 dismissed a stale review via 1a1c02a September 21, 2026 16:53
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Round 3 closeout — three of the five nits fixed, two deferred.

1a1c02ae corrects the three that were wrong in text this PR itself added. Each re-verified against the tree rather than taken from the review:

Deferred, both now disclosed under Risk & Scope:

  • The ~276 token figure. Real staleness, but it is upstream drift rather than a defect at this head: the pinning test still reads 1,104 characters (~276 tokens) at this PR's base (da695fea) and only became 1,421 characters (~355 tokens) on main afterwards, once the monitor bullet got gated too. Correcting it here means merging 10 commits of main to stay self-consistent, and base movement on its own is not a reason to push. It belongs with the next main merge.
  • §10's stale "less than a tenth" sentence. Fixing it in place would pull docs/plans/…:300 — currently a context line — into the diff. The top banner already names and restates it, so a reader who lands on §10 is one hop from the correction.

The description now also covers docs/verification/context-token-governance/README.md (+97/−14, the entire new §0.1), which was the largest change in the diff and previously absent from it, and says plainly that §0.1's #12096 / #12198 upgrade-risk rows sit past the goal in its first line.

Scope ledger, recorded below so the next pass does not restart at round 0. Baseline is the earliest reviewed head e6e4c2db; docs went +8/−6 across 2 files → +119/−30 across 3, and this round added none of that — post-fix totals are identical to round start (4 files, +126/−30, implementation 0). The growth is author-driven (225a8925, a concurrent re-baseline of the handoff doc), not review-induced, so there was nothing to revert; the correct move was to make the description match the diff. This is substantive round 3, which is the limit — a next round needs a human decision rather than another automatic pass.

Prettier --check is clean on both files; the README's §6 table re-aligned, so 16 of its 24 changed lines are whitespace. No source touched, so no suite re-run. The push re-triggered review-pr on this head and dismissed the previous approval as dismiss_stale_reviews requires, leaving the PR at 0/2 — it needs the bot's re-review plus one human approval.

Unrelated to this PR's code: the sandboxed verification run 35621014837 from yesterday's /triage is still in_progress after ~24h, and its comment is stuck at verify-state=running. Worth cancelling.

yiliang114 and others added 2 commits September 22, 2026 05:53
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
@wenshao

wenshao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Local real-environment verification: PR #12355 @ 1b956a82

Verdict: mergeable. No production code changed, the new test row is proven load-bearing on macOS, and the trial merge into current main is clean. The central rewrite in context-cost.md holds on a real provider: the declared list stays byte-stable across a bridge discovery, the prompt-cache prefix survives, and removing a bridge half behaves asymmetrically. I found one measurable error in text this PR adds (A), which I'd fix before merging; it is a two-sentence edit. B and C are wording nits that can go to a follow-up.

The sandboxed run (35621014837) used a scripted model in a container. This run covers what its "Not covered" section left open: a real model on a real provider (prefix-cache hits reported by the provider), a real stdio MCP server, the real interactive TUI, released v0.24.1/v0.24.2 as the before/after arms, threshold > 0, all four bridge-removal seams, the version-range and commit-sha claims (full history), and a trial merge into today's main.

Setup

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.

real model prefix cache

  • PR head: tool_search("select:write_file") → tool_call(write_file). The tools sha is e6f6413a… 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 tools array (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

bridge seams and MCP preload

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.

TUI context detail aggressive allowlist

  • /context detail shows Built-in tools going from 6.9k to 6.3k. On the first real request, the declared tools drop 15 → 13, and the provider's prompt_tokens go 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.ts toolMatchesRuleToolName). read_file admits zoom_image (READ_TOOLS) and run_shell_command admits monitor. Both are on demand by default, so v0.24.1's threshold-10 preload declared them, along with the exempt task_stop. The on-demand pool was therefore not "already excluded" under these allowlists. The +67 counts the floor change only (+148 tool_call, +19 tool_search, −102 task_stop) and misses −467 monitor, −279 zoom_image.
  • The same mechanism explains the task_stop 102 in 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_image via read_file, monitor via run_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

unit test, merge and claims

  • Every commit sha, date and count in §0.1 checks out against full history: 70edbf47 has 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.enabled unset, both versions honour an undecided workspace's settings. With it set to true, 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 plus tool_call. On the same build, threshold 10 → 0 moves /context Built-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 with threshold: 10 reads 15.6k (the gap is roughly tool_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_image and web_fetch. Under the conservative allowlist, the subagent loses the demoted tools but keeps the family-admitted on-demand ones.
  • /context detail shows tool_search 394 and tool_call 148, 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 only Command executed successfully., on v0.24.1, head and main. The detail subcommand awaits the main action but drops its returned message (contextCommand.ts on main, around line 1041). /context -d works.
  • When skill is itself demoted (for example tools.eager: []), the Built-in tools category reads 0 while its per-item list sums to 542. The category subtracts the skill schema even when skill is not declared. §2.2's lists keep skill, 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 次请求的 tools sha 都是 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_search 394、tool_call 148,与 §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 只改了文档和一行测试)。

wenshao
wenshao previously approved these changes Sep 22, 2026
@wenshao

wenshao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge September 22, 2026 11:50
@yiliang114
yiliang114 requested a review from chiga0 September 22, 2026 12:53
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
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Doc-accuracy pass — all six findings fixed, pushed as 1df0ecfd.

@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 1b956a82 before editing; nothing was taken on faith from either report. No production code touched — the commit is +41/−41 across the same four files.

A · fixed as you prescribed (README §2.5 + §8 item 1). The offending text was there verbatim: §2.5 said 「这两个预期值都是 地板 + 白名单内 schema,不含按需池 … 读数会比上表高约 67,这是正常的,不是配置没生效」, §8 item 1 said 「方向不是"≥ 0.24.2 上更低"——恰恰相反,会略高一点」 and 「别把"读数比 9,481/5,868 高几十"当成配置没生效」. Both direction sentences are gone. §2.5 now reads:

这两个预期值算的是 地板 + 白名单内 schema(激进版 = 2,163 + (4,080 − 375)),但别把它当成"不含按需池":白名单条目会按族放行(rule-parser.ts 的 toolMatchesRuleToolName)——read_file 带出 zoom_image(READ_TOOLS)、run_shell_command 带出 monitor,这两个默认都是按需工具,所以 v0.24.1 的 threshold-10 预加载本来就把它们(连同豁免的 task_stop)声明了出来,那份预期值里含着按需池的一部分。≥ 0.24.2 上有两股相反的力:地板从 2,163 涨到 2,230(+67,见 §2.1 的更正),同时这批按族放行的按需工具不再被预加载(实测 monitor −467、zoom_image −279)。净方向取决于白名单,不是"一定略高":评审在真实 TUI 里用 §2.2 的激进版量到 −681(首轮声明工具 15 → 13,provider 的 prompt_tokens 15,912 → 15,231),保守版约 −0.8k(-p,9.0k → 8.2k)。所以读数比上表低几百同样正常,不是配置没生效。

§8 item 1 carries the same correction plus your −467/−279 split; it keeps +67 and the 2,230 + 3,705 = 5,935 / 9,548 arithmetic but explicitly as "只算地板这一项", and ends with 「读数比 9,481/5,868 低几百或高几十都可能是正常的」. Your third bullet is applied too: §2.1's 更正 note now says 「但"本来就不常驻"是 ≥ 0.24.2 的说法,不适用于它所更正的那份 v0.24.1 读数:旧读数里那 102 是 v0.24.1 的 threshold-10 预加载把 task_stop 每次请求都声明了出来」.

B · minimal wording only, no restructuring. context-cost.md's threshold bullet gained one sentence: "The preload also runs at session start, and MCP servers usually connect after it, so a fresh launch declares no MCP tools whatever the threshold; they reach the preload from the next session start (in the CLI, after /clear), or immediately under QWEN_CODE_LEGACY_MCP_BLOCKING=1." In README §0.1 I softened the MCP row rather than dropped it: the cells now name the ordering cause (Config.initialize() → llmClient.initialize() → startChat preload, then background discovery), record your 0/8 on v0.24.1, and move the row's real bite to the "servers already connected at session start" path — with 「daemon 的池化 MCP,未实测」 stated, since you did not test it. The 怎么尽快发现 cell now warns that MCP reading 0 is also v0.24.1's CLI behaviour, so it is not by itself a regression signal. Reason for softening instead of dropping: this README is written for the daemon/ACP deployment, where the row still says something true. Say the word and it goes. settings.md untouched — outside this diff.

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: followup/speculation.ts:338 toolRegistry.isPermissionDeferred?.(name) and skills/workflow-authoring-skill.ts:211 isToolDeferredBehindToolSearch.

Sandbox Finding 2 · fixed. Verified independently: this branch's own prompts.test.ts still reads saves about 1.1k characters / 1,104 characters (~276 tokens) / <1_400, while 733f58b6 — the main this PR merges into — reads saves about 1.4k characters / 1,421 characters (~355 tokens) with the monitor bullet gated too / <1_500. The PR does not touch that file, so the merged tree carries 1,421 and the deferral would have landed the stale number. Both re-emitted rows now say 1,421 字符 ≈ 355 token: plan-doc §10's #12032 row (with the 1,104 = subagent + codebase, 317 = monitor split spelled out) and README §0.1's §5 row. The PR body's deferral sentence is rewritten in both languages. The two other copies the sandbox named (docs/verification/resident-tool-prompt-assembly/README.md:40, docs/design/2026-09-18-resident-tool-prompt-assembly{,.zh-CN}.md:123) are outside this diff and stay out, as the sandbox itself scoped them.

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 main: tools/tools.ts:233-240 holds version?: string; } + the ToolBuilder docblock; 241-246 holds TResult extends ToolResult, / > {; config/config.ts:6969-6977 holds getProjectRoot() / getCwd(); permission-manager.ts:848-866 lands on isExemptFromEagerAllowList (declared at main:846) — the function this PR's new test row targets, exactly as the sandbox said. They now cite DeclarativeTool 构造参数 shouldDefer / alwaysLoad, getVisibleTools()(读构造期存入的 visibleTools), and getToolRegistrationStatus()(deny 分支 return 'disabled'), matching the rows that already used symbols. The three ranges the sandbox confirmed accurate (tool-registry.ts:384-419, :289-321, client.ts:1746-1774) are left alone to keep the diff minimal; if you want the whole table symbol-only, that is a one-line follow-up.

Local verification. npx vitest run src/permissions/permission-manager.test.ts → 514/514, run twice (before and after the commit). npx prettier --check on all four changed files → clean (the two markdown files needed --write for table realignment; done, then re-checked). npm run typecheck in packages/core → 73 errors, 0 in any file I touched; 51 are TS2307 cannot find module (this worktree's node_modules is missing ajv, ignore, quickjs, grpc types) and the rest are knock-on implicit-any in src/telemetry/* and src/code-mode/host.ts. Same reason npm run build cannot complete here. Environment noise, not a regression from a comment-and-markdown commit.

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.

@yiliang114
yiliang114 requested a review from wenshao September 22, 2026 15:09

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_000 is declared at config.ts:362 and its only computational use is thresholdTokens = Math.min(ratioTokens, MEMORY_CONTEXT_WARNING_MAX_TOKENS) at 4977-4980. The enclosing buildMemoryContextWarning returns undefined below the threshold and a warning string above it; memoryContent is used only to derive estimatedTokens and 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 in return this.trustedFolder ?? !this.folderTrust (line 10122 region) and this.folderTrust = params.folderTrust ?? false (line 3339). With the switch off, !false is true, 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: 10 rollback 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.

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE

@wenshao
wenshao added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 8af8b9d Sep 22, 2026
68 of 69 checks passed
@chiga0

chiga0 commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

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.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

发布记账(本 PR 已于 2026-09-22 15:45Z 合并为 8af8b9df)

已给本 PR 补上 type/documentation 标签,因为标题的 conventional type 会把它归错段:

scripts/generate-release-notes.js 的 classifyChange() 先看标签,再用标题 type 兜底查 TYPE_CATEGORIES。本 PR 合并时标签为空、标题是 fix(core,docs): → 兜底命中 fix → Bug Fixes。而 compactEntry() 是把这个算好的 category 直接交给模型去组装 curated release notes 的,模型并不重新判段。实际 diff 是 3 篇文档 + 1 个测试文件(packages/core/src/permissions/permission-manager.test.ts,+7/−0),没有任何生产代码改动,归进 Bug Fixes 会让 CHANGELOG.md 出现一条并不存在的修复。type/documentation 在 classifyChange() 里的优先级高于 type 兜底,且标签在 PR 合并后依然有效,所以下次切版(v0.24.5)这一条会落到 Documentation 段。

标题不再改:squash 提交信息已经定死在 main 上,改 PR 标题不会影响它。


另一件同类的事已经发生,且还没解决:#10410(70edbf47)随 v0.24.2 发布(git tag --contains 70edbf475e 含 v0.24.2;该 release 09-20 14:12Z 列出 feat(core): preserve prompt cache for deferred tools),它把 tools.toolSearch.threshold 的默认值从 10 改成 0——一个默认行为变更。但 v0.24.2 的 release body 和 CHANGELOG.md 的 0.24.2 段写的都是「No known breaking changes.」。

机制与本 PR 正文预警的相同,只是判定点不是 ! 标记:classifyChange() 进 Breaking Changes 的条件是带 breaking-change 标签、或标题匹配 /^\w+(?:\([^)]*\))?!:/,#10410 两者都没有(标签为空,标题无 !)。

现在给 #10410 补标签已经没有作用:v0.24.2 的 body 早已发布,且该 PR 不会再落进后续版本的提交区间。唯一的补救是编辑已发布的 v0.24.2 release body 加一行披露,再跑 npm run changelog 重生成 CHANGELOG.md。这属于修改已发布产物并向 main 推送,没有在这条备注里顺手做掉。

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

Labels

type/documentation Documentation improvements or additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants