fix(core): sync loaded-skill state with history eviction; add user /unskill command - #8900
fix(core): sync loaded-skill state with history eviction; add user /unskill command#8900ZijianZhang989 wants to merge 16 commits into
Conversation
…nskill command History rewrites (pre-send microcompaction, /compress-fast, memory-pressure compact_history, LLM /compress) blanked skill bodies without updating the loaded-skill set. The dedup guard then answered every re-invocation with 'already loaded in context', leaving the skill permanently unusable while /context kept reporting it active. Microcompaction now reports blanked skill names in its meta; all four rewrite paths consume it to sync the tracking (targeted unload, wholesale clear when a name cannot be resolved); and a new user-only /unskill <name> command replaces a loaded skill's body with a placeholder, adjusts the token estimate downward, and re-arms the dedup guard so the next invocation reloads the full body. Closes #6762
|
|
E2E 测试报告验证环境:macOS · 本分支构建产物( V1 驱逐同步(/compress-fast 路径)— PASS
V2 /unskill 命令 — PASS
V3 最终 bundle 冒烟 — PASS在补齐全部测试缺口 + 命令文件 kebab-case 重命名后的最终 bundle 上复验:active +103 → 验证 gotcha(供复测参考)
单测与质量门
|
|
Thanks for the PR! Template looks good ✓ Problem: observed bug, confirmed in the current code. Direction: aligned — this is squarely the context-lifecycle gap the roadmap item describes. Claude Code's CHANGELOG has no direct analogue of an unload command, but skills are an active area there too (fork/background skills, model-invocation controls), and the eviction-sync half is a correctness fix regardless of direction debates. Size: touches core paths. Production logic ≈393 lines vs tests ≈713 lines — under the 500-line maintainer-awareness threshold, no large-PR advisory. Approach: the scope feels right. The eviction-sync half mirrors the existing issue-#4239 pattern ( Risk: Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题: 已观测到的 bug,且在当前代码中确认存在。 方向: 对齐——这正是 roadmap 条目所描述的上下文生命周期缺口。Claude Code 的 CHANGELOG 没有卸载命令的直接对应物,但 skills 在那边也是活跃领域(fork/background skills、模型调用控制),且驱逐同步这一半本身就是正确性修复。 规模: 触及核心路径。生产逻辑约 393 行,测试约 713 行——低于 500 行维护者关注阈值,也不触发大 PR 建议。 方案: 范围合理。驱逐同步一半沿用了 issue #4239 的既有模式( 风险: 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewI formed an independent proposal before reading the diff — report evicted skill names via the request-side What I verified against the code:
Findings — none blocking. One follow-up worth filing:
The eviction-sync flowsequenceDiagram
participant P1 as User
participant P2 as History rewrite path
participant P3 as MicrocompactMeta
participant P4 as syncSkillEvictions
participant P5 as SkillTool tracking
P1->>P2: sends a message or runs a compress command
P2->>P2: blanks old skill bodies from history
P2->>P3: evictedSkillNames plus unresolvedEvictedSkills
P3->>P4: consumed at all four rewrite sites
alt every eviction resolved
P4->>P5: unloadSkills with the named skills
else any eviction unresolved
P4->>P5: clearLoadedSkills blanket clear
end
P5-->>P1: dedup guard re-arms, next invoke reloads the full body
Files changed (15)
Testing evidenceThis is an unattended CI run — the PR's code is never executed here. Evidence below is the PR's own CI, fetched once via the API (no polling; the Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle this: Not verified: end-to-end TUI behavior on Linux/Windows — no independent live run in this pass (unattended CI). 中文说明代码审查:先独立构思方案再对照 diff——本 PR 的做法与我独立提出的方案基本一致:通过请求侧 functionCall 参数恢复被清空的 skill 名(与 issue #4239 的文件读取机制同一模式)、在每个历史改写点取消跟踪、无法归因时整体清空。没有遗漏更简单的路径。 已核对:三个 无阻塞项。一个后续建议: 测试证据:本次为无人值守 CI 运行,不执行 PR 代码。以上证据来自 PR 自身 CI(API 一次性抓取,不轮询;CI 结束后由 finalize 任务更新表格)。抓取时无红色检查;ubuntu 单测套件(本改动的权威验证)仍在运行,macOS/Windows 单测作业被仓库 CI 画像分类跳过(非失败)。行为层面的最终确认建议由维护者触发 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — solid, well-scoped fix that matches the idiomatic pattern in this codebase; only non-blocking nits (an uncovered Stepping back: this PR earns its merge. The ghost-skill state is a real dead end I confirmed in the current code — once compaction blanks a body, the skill is unrecoverable for the rest of the session — and the fix extends the exact mechanism this repo already built for the analogous file-read problem, which is the right instinct. The reservations, named plainly: Approval deferred until CI lands green on 中文说明信心度:4/5 —— 扎实、范围得当的修复,与本代码库的惯用模式一致;仅有非阻塞的小问题( 总体评价:这个 PR 配得上合并。幽灵 skill 状态是我在当前代码中确认过的真实死路——压缩清空正文后,该 skill 在会话余下时间里无法恢复——而修复方案恰好扩展了本仓库为同类文件读取问题已建立的机制,这是正确的直觉。 坦率地说保留意见有两点: 批准推迟到 CI 在被审提交上全绿——审查时 ubuntu 单测仍在运行,不会为尚不存在的结果背书。CI 全绿后由 finalize 任务发布绑定该提交的批准。 — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — finished within budget; no check was left unfinished..
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — finished within budget; no check was left unfinished.。
— qwen3.8-max via Qwen Code /review (v0.21.9)
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— deepseek-v4-flash via Qwen Code /review (v0.21.8)
- microcompaction: suppress a skill's eviction report when a kept result still holds its reloaded body (a stale dedup confirmation from an earlier load cycle no longer un-tracks a resident skill); treat the /unskill placeholder as already-cleared so it neither absorbs a keepRecent protection slot nor gets re-blanked - hooks: dedup registerSkillHooks so unload/reload cycles don't stack duplicate session hooks - /unskill: reject names that are not real skills (the skill tool's command-executor fallback also tracks command names); fall back to locating the body in history when in-memory tracking was lost (--resume); filter completion candidates to real skills; fix import ordering in BuiltinCommandLoader - i18n: add the command's strings to the en baseline and all 8 locale files, fixing the strict-parity mustTranslateKeys CI failure - tests: BuiltinCommandLoader registration test plus review-driven cases for all of the above
Review round 2 — all findings addressed in
|
The dedup compares stored SessionHookEntry.config values, whose type includes function/prompt hooks; keying those by a best-effort JSON blob (they never originate from skill frontmatter, so they simply never match).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): chunk 1: did not run BuiltinCommandLoader.test.ts (worktree has no node_modules; install was out of budget) — test verified by inspection only..
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):chunk 1:did not run BuiltinCommandLoader.test.ts (worktree has no node_modules; install was out of budget) — test verified by inspection only.。
— qwen3.8-max via Qwen Code /review (v0.21.9)
R2-1 (Critical): buildKeptSkillNames counted SkillTool error outputs
("Skill x not found."/"is disabled."/"Failed to load") as residency proof,
suppressing eviction and leaving the skill permanently unreloadable behind
the dedup guard. Switch to a positive body check (the buildSkillLlmContent
"Base directory for this skill:" prefix) so only a real body proves
residency; errors/confirmations/placeholders/cleared messages do not.
R2-3: an ambiguous call-id (one id mapped to multiple skill names) now
protects NONE, matching buildKeptFilePaths' length!==1 guard.
R2-9: filter on the clear-set (not the keepRecent set) so a body that
survives the size path via the low-watermark early break is no longer
over-un-tracked (token doubling).
R2-10: reword the evictedSkillNames field doc, which still claimed "No
kept-suppression" — flatly contradicting buildKeptSkillNames above it.
R2-6: hasSkillBodyInHistory now returns true only for a body OR a dedup
confirmation, excluding SkillTool error text (so /unskill after --resume
no longer claims a body exists for a failed/disabled load).
R2-5: the cached-skills gate no longer rejects a skill deleted/renamed
mid-session (body still in history) — it falls through to the resume
fallback instead of mislabeling it as a command.
R2-15 (1/3): wire clearLoadedSkillTracking into truncateHistory so /rewind
past a skill load no longer leaves the dedup guard blocking every reload.
R2-8: addSessionAllowRule deduplicates on raw, mirroring addPersistentRule
and the dangerous-stash branch, so reload cycles stop accumulating the
skill's allowedTools list.
Tests: hasSkillBodyInHistory unit coverage (R2-11); kept-suppression cases
for error-output (R2-1) and ambiguous call-id (R2-3).
Deferred to a follow-up issue: R2-2/R2-7 (hook dedup depth +
unregisterSkillHooks wiring), R2-14 (false warn on fully-deduped reload),
R2-4 (completion after --resume), R2-12/R2-13 (error/HTTP branch tests),
R2-15 2/3+3/3 (/restore + ACP restoreHistory + startNewSession).
|
已提交修复 已修复(已标记 resolve)
延后到 follow-up issue(未标 resolve,已在各线程逐条回复)
全部 typecheck + 单测通过(microcompact 86、geminiChat 328、unskill-command 10、registerSkillHooks 9、permission-manager 332、skill 82、client truncateHistory 4)。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; chunk 8: did not execute npx vitest run src/tools/skill.test.ts — the review worktree has no node_modules (checked worktree root and packages/core ), so running tes…; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks completed within budget., and 3 more.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
中文说明
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;chunk 8:did not execute npx vitest run src/tools/skill.test.ts — the review worktree has no node_modules (checked worktree root and packages/core ), so running tes…;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks completed within budget.,另有 3 条。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
— qwen3.8-max via Qwen Code /review (v0.21.9)
…arity R3-7 (Critical): syncSkillEvictions was called AFTER disarmFileReadCacheAfterEviction; if disarm threw, sync was skipped while setHistory had already committed, leaving the skill tracking in a ghost state that blocked reloads. Swap the two lines so sync runs first (it is synchronous and internally guarded), then disarm (failure only degrades the file-read cache). Fixed in both microcompaction and compress-fast call sites. R3-14 (Critical): stripOrphanedUserEntriesFromHistory (failed-then-retry path) stripped user turns containing skill bodies but only cleared the FileReadCache, not the loaded-skill tracking — same ghost deadlock as R3-7. Added clearLoadedSkillTracking after the cache clear. R3-2: unloadSkillBody matched a body to the wrong skill when a call-id was shared by multiple skill invocations. Now refuses to clear when the mapping is ambiguous (length !== 1). R3-16: unloadSkillBody treated SkillTool error text (e.g. "Skill x not found.") as a clearable body. Now guarded by isSkillBodyOutput, so only a real body is cleared. R3-3: The eviction-record block in microcompaction counted any SkillTool part with a non-cleared response as a kept body, including error outputs. Added isSkillBodyOutput guard so only real bodies suppress the eviction report. R3-12: Catalan locale used "La habilitat" (phonetically wrong for the elision). Fixed to "L'habilitat" in 3 places. R3-10: mustTranslateKeys.ts was missing the 8 unskill-command i18n keys, so the strict-parity check would not catch missing translations in fork locales. R3-4/R3-5/R3-6: Added tests for truncateHistory tracking clear (2), addSessionAllowRule dedup (1), and mid-session deleted skill bypass (1).
R3-3's isSkillBodyOutput guard requires the buildSkillLlmContent prefix to recognize a skill body. Two test fixtures (client.test.ts and memoryPressureMonitor.test.ts) used a bare 'skill body '.repeat(50) string without the prefix, causing the guard to skip the eviction record and the sync to never un-track the skill — failing the assertion.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5; round-5 findings left unverified (compose-floor budget stop).
Not explored to full depth (tool budget reached): PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: none — all checks above completed within budget.; PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: none — all planned checks completed within budget.; chunk 8: could not execute packages/core vitest ( skill-utils.test.ts , skill.test.ts ) — the review worktree has no installed node_modules and vitest fails to load i…; chunk 1: running packages/cli vitest suites for BuiltinCommandLoader.test.ts and mustTranslateKeys.test.ts (dependencies not installed).; PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: (none — all planned checks completed within budget)., and 3 more.
— [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.
中文说明
已审查。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:reverse audit — did not converge within the reverse-audit round cap of 5; round-5 findings left unverified (compose-floor budget stop)。
未探索到全部深度(达到工具调用预算):PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:none — all checks above completed within budget.;PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:none — all planned checks completed within budget.;chunk 8:could not execute packages/core vitest ( skill-utils.test.ts , skill.test.ts ) — the review worktree has no installed node_modules and vitest fails to load i…;chunk 1:running packages/cli vitest suites for BuiltinCommandLoader.test.ts and mustTranslateKeys.test.ts (dependencies not installed).;PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:(none — all planned checks completed within budget).,另有 3 条。
— [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。
— qwen3.8-max via Qwen Code /review (v0.21.9)
Replace negative-enumeration skip (error key / placeholder / cleared message) with positive check: isSkillBodyOutput || isSkillDedupConfirmation. SkillTool error texts (plain response.output with no error key) are now naturally skipped instead of being rewritten into success-shaped placeholders. Remove unused MICROCOMPACT_CLEARED_MESSAGE import. Test fixture updated to use buildSkillLlmContent prefix so the body passes the positive check.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8900 syncs loaded-skill tracking with history rewrite...: none — all checks I planned completed within budget.; PR #8900 syncs loaded-skill tracking with history rewrite...: none — all checks above were completed within the tool budget.; PR #8900 syncs loaded-skill tracking with history rewrite...: did not trace the final top-level rendering of the unhandled rejection in the interactive path beyond confirming slashCommandProcessor.ts has no try/catch aro…; PR #8900 syncs loaded-skill tracking with history rewrite...: none — all checks above completed within budget..
中文说明
已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8900 syncs loaded-skill tracking with history rewrite...:none — all checks I planned completed within budget.;PR #8900 syncs loaded-skill tracking with history rewrite...:none — all checks above were completed within the tool budget.;PR #8900 syncs loaded-skill tracking with history rewrite...:did not trace the final top-level rendering of the unhandled rejection in the interactive path beyond confirming slashCommandProcessor.ts has no try/catch aro…;PR #8900 syncs loaded-skill tracking with history rewrite...:none — all checks above completed within budget.。
— qwen3.8-max via Qwen Code /review (v0.21.10)
Move clearLoadedSkillTracking calls from GeminiClient wrappers down into GeminiChat.truncateHistory and GeminiChat.stripOrphanedUserEntriesFromHistory so both TUI and ACP paths are covered (ACP sessions call GeminiChat methods directly, bypassing client-level wrappers). Remove now-redundant client-level calls and unused import. Remove two client-level tests that verified the clear at the wrong layer.
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
中文说明
已审查。 2 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: none — all checks above completed within budget.; PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: none — all checks above completed within budget.; PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: none — all planned checks completed within budget (~24 tool calls).; PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...: none — all planned checks completed within budget..
中文说明
已审查。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:none — all checks above completed within budget.;PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:none — all checks above completed within budget.;PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:none — all planned checks completed within budget (~24 tool calls).;PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking ...:none — all planned checks completed within budget.。
— qwen3.8-max via Qwen Code /review (v0.21.10)
|
@qwen-code /resolve |
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge summary — PR #8900 ← mainRoot cause. Main's PR #8365 ( Textual, not semantic. Both sides only add keys; neither touched existing logic. Resolution keeps both additions in all 9 locales ( 'Kept model as {{model}}': 'Kept model as {{model}}',
// /unskill command
'Usage: /unskill <skill-name>': 'Usage: /unskill <skill-name>',
…
};Union verified: merged tree vs Load-bearing invariants (breaking these fails CI's
Could not verify. No build/tests/lint run (per instructions; deps absent). Both-sides auto-merged areas: main's 中文说明冲突根因:main 的 PR #8365( 纯文本冲突,非语义冲突:双方都只是新增键,未改任何已有逻辑。解决方式是在全部 9 个语言文件中同时保留两边新增内容(main 的键在前, 关键不变量(破坏会挂 CI 的 未能验证:按指令未运行构建/测试/lint(依赖未安装)。双方都改动的自动合并区域:main 的 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
已审查。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.21.10)
R6-2: Add length guard to truncateHistory (only clear when history actually shrank) and strippedEntries guard to stripOrphanedUserEntriesFromHistory (only clear when entries were actually removed). Prevents no-op truncates/strips from clearing skill tracking and causing duplicate body injection. R6-4: Update clearLoadedSkillTracking JSDoc to reflect all call sites (tryCompress, truncateHistory, stripOrphanedUserEntriesFromHistory) instead of just tryCompress.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "You are review agent reverse-audit — Reverse audit agent…": none — all checks I started were completed within budget (~20 tool calls).; "You are review agent reverse-audit — Reverse audit agent…": none material — all planned checks completed.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — finished within budget (~20 calls).; "You are review agent reverse-audit — Reverse audit agent…": none — all checks I started completed within budget., and 4 more.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"You are review agent reverse-audit — Reverse audit agent…":none — all checks I started were completed within budget (~20 tool calls).;"You are review agent reverse-audit — Reverse audit agent…":none material — all planned checks completed.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — finished within budget (~20 calls).;"You are review agent reverse-audit — Reverse audit agent…":none — all checks I started completed within budget.,另有 4 条。
— qwen3.8-max via Qwen Code /review (v0.21.10)
…and retry restore R7 review fixes: - Forked/speculative chats (createForkedChat) share the parent's ToolRegistry while compressing only a copy of a parent-history slice. Their tryCompress must not blanket-clear the shared loaded-skill tracking, which would disarm the parent's dedup guard while its bodies stay resident. Mark forked chats via isForkedChat and skip the clear there. - The retry strip path clears loaded-skill tracking, but restoreStrippedRetryEntries can put stripped skill bodies back without re-registering them. Re-track restored skill bodies (resolve call ids via buildCallIdToSkillName) so the dedup guard matches the resident bodies again. Adds SkillTool.trackSkills and skill-utils retrackSkills helpers.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — reached the 5-round cap without two consecutive dry rounds (chunk 5 still reported in round 5; that finding was verified low-confidence and moved to Needs Human Review).
Not explored to full depth (tool budget reached): "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all checks above were completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": did not execute the rewind scenario end-to-end in a live TUI session to observe the duplicate injection; the finding rests on code-path tracing of computeApiTr…; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget (~30 tool calls).; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget., and 9 more.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:reverse audit — reached the 5-round cap without two consecutive dry rounds (chunk 5 still reported in round 5; that finding was verified low-confidence and moved to Needs Human Review)。
未探索到全部深度(达到工具调用预算):"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all checks above were completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":did not execute the rewind scenario end-to-end in a live TUI session to observe the duplicate injection; the finding rests on code-path tracing of computeApiTr…;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget (~30 tool calls).;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.,另有 9 条。
— qwen3.8-max via Qwen Code /review (v0.21.11)
Address R9 review comments: - strip: un-track only skills whose bodies were provably stripped, instead of blanket-clearing all tracking - truncate / hard-rescue restore / retry restore: rebuild tracking from the (re-)resident history instead of leaving a blanket clear - ACP continuation strip: reconcile tracking in the finally block once the continuation settles - client retry: re-track in both the restore and re-push branches - microcompact size path: use pending-filtered refs for kept skill names, matching buildKeepRefs' pending exclusion Tests: isForkedChat marking, trackSkills dedup re-arm, strip/truncate tracking behavior, and ACP continuation reconcile assertions.
The reconcileLoadedSkillTracking mock was added twice to the Session test chat stub, which tsc --build rejects (TS1117).
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
Not explored to full depth (tool budget reached): This PR syncs loaded-skill tracking with history eviction...: none — stayed well within the soft ceiling..
中文说明
已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
未探索到全部深度(达到工具调用预算):This PR syncs loaded-skill tracking with history eviction...:none — stayed well within the soft ceiling.。
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — reached the 5-round cap without two consecutive dry rounds (round 5 still reported findings; those findings were verified afterwards).
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all planned checks completed within budget.; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all planned checks completed within budget.; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all planned checks completed within budget.; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none (finished under budget; no check left incomplete).; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all checks above completed within budget., and 7 more.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:reverse audit — reached the 5-round cap without two consecutive dry rounds (round 5 still reported findings; those findings were verified afterwards)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all planned checks completed within budget.;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all planned checks completed within budget.;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all planned checks completed within budget.;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none (finished under budget; no check left incomplete).;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all checks above completed within budget.,另有 7 条。
— qwen3.8-max via Qwen Code /review (v0.21.11)
- Guard hard-rescue restore reconcile against forked chats, matching the adjacent tryCompress clear - Re-track stashed skill names at conversation finish so the abort exit is covered; reconcile retry restore against settled history so never-re-pushed entries stay untracked - Resolve orphan skill names at strip time so compaction between strip and settle cannot break call-id pairing - Dedupe reused call-ids per skill name at the source; read-only pairing walk uses the shallow history variant - Add call-site tests for retry push-landed/ghost branches, zero-survivor truncate, and oversized restore re-track
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — reached the 5-round cap without two consecutive dry rounds (round 5 still reported findings; those findings were verified afterwards).
Not explored to full depth (tool budget reached): "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all checks above completed within budget.; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": nothing significant — everything I intended, I completed. Should I note that I didn't run the full i18n index.test.ts? That's minor. Note that I didn't walk the…; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all planned checks completed within budget; core blanking/placeholder internals beyond the refusal branch belong to another chunk and were not re-audited; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — finished within the tool budget.; "PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…": none — all planned checks completed within budget., and 11 more.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:reverse audit — reached the 5-round cap without two consecutive dry rounds (round 5 still reported findings; those findings were verified afterwards)。
未探索到全部深度(达到工具调用预算):"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all checks above completed within budget.;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":nothing significant — everything I intended, I completed. Should I note that I didn't run the full i18n index.test.ts? That's minor. Note that I didn't walk the…;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all planned checks completed within budget; core blanking/placeholder internals beyond the refusal branch belong to another chunk and were not re-audited;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — finished within the tool budget.;"PR #8900 (QwenLM/qwen-code): syncs loaded-skill tracking…":none — all planned checks completed within budget.,另有 11 条。
— qwen3.8-max via Qwen Code /review (v0.21.11)
Address review round 11: replace the additive re-track at ACP settle with a residency-aware reconcile (removing the retrackSkills primitive), filter strip un-tracking by resident bodies, guard /unskill before first send, and correct the strip docstring.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget., and 1 more.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all planned checks completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all planned checks completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.,另有 1 条。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| export function isSkillBodyOutput(output: unknown): boolean { | ||
| return typeof output === 'string' && output.startsWith(SKILL_BODY_PREFIX); | ||
| } |
There was a problem hiding this comment.
[Suggestion] R12-4: isSkillBodyOutput is a single-prefix check, so a command-executor-fallback Skill result whose output happens to start with Base directory for this skill: is accepted as residency PROOF — the false-positive mirror of the tracked-command-result reporting gap (the enabled command fallback tracks command names via onSkillLoaded, and command output is arbitrary text). Probe-verified on the unmodified PR for all three consequences: (a) a kept sentinel-prefixed command result suppresses the genuine eviction report when the skill's real body is blanked in the same pass (evictedSkillNames stays [] → ghost tracking); (b) reconcileLoadedSkillTracking re-tracks the name from a spoof-only history; (c) hasSkillBodyInHistory returns true for a tracked command name, opening the /unskill mid-session-deleted-skill bypass, and unloadSkillBody then blanks the command's actual output into the unskill placeholder (cleared: true measured). — Failure scenario: an MCP prompt or file command emitting text that begins with the sentinel string (shapeable by prompt content) — every step of the mechanism was run-verified; trigger frequency is the only uncertainty, hence Suggestion. The flip check passed: requiring the second static line buildSkillLlmContent always emits reverses every observation while real bodies still pass.
| export function isSkillBodyOutput(output: unknown): boolean { | |
| return typeof output === 'string' && output.startsWith(SKILL_BODY_PREFIX); | |
| } | |
| export function isSkillBodyOutput(output: unknown): boolean { | |
| return ( | |
| typeof output === 'string' && | |
| output.startsWith(SKILL_BODY_PREFIX) && | |
| output.includes( | |
| 'Important: ALWAYS resolve absolute paths from this base directory when working with skills.', | |
| ) | |
| ); | |
| } |
中文说明
[建议] R12-4:isSkillBodyOutput 只做单前缀检查,因此命令执行器回退(command-executor fallback)产生的 Skill 结果只要输出恰好以 Base directory for this skill: 开头,就会被当作"正文仍在"的证据——这正是"被跟踪的命令结果不被驱逐上报"缺口的误报镜像(启用的命令回退会通过 onSkillLoaded 跟踪命令名,而命令输出是任意文本)。已在本 PR 未修改代码上用探针验证了全部三个后果:(a) 一个被保留的、带哨兵前缀的命令结果,会在同一轮清空该 skill 真实正文时压制真正的驱逐上报(evictedSkillNames 保持 [] → 幽灵跟踪);(b) reconcileLoadedSkillTracking 会基于只剩伪造结果的历史重新跟踪该名字;(c) hasSkillBodyInHistory 对被跟踪的命令名返回 true,打开 /unskill 的"会话中途被删 skill"旁路,unloadSkillBody 随即把该命令的真实输出清空成 unskill 占位符(实测 cleared: true)。失败场景:某个 MCP prompt 或文件命令输出了以该哨兵字符串开头的文本(prompt 内容可以人为构造)——机制的每一步都已实测验证,唯一的不确定性是触发频率,因此定为建议级。翻转验证通过:要求同时匹配 buildSkillLlmContent 必然输出的第二行静态文本后,上述所有观测全部翻转,而真实正文仍然通过。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| this.#getCurrentChat().reconcileLoadedSkillTracking( | ||
| 'acpContinuationSettle', | ||
| ); |
There was a problem hiding this comment.
[Suggestion] R12-5: The settle reconcile re-derives residency via call-id pairing at settle time, but #sendMessageStreamWithAutoCompression runs tryCompressChat (Session.ts:5837) BEFORE chat.sendMessageStream pushes the continuation parts (geminiChat.ts:2641). — Failure scenario: ACP continuation of an interrupted turn whose orphan tail carries skill X's body → the strip un-tracks X and resolves names at strip time → a pre-push LLM-summary compression replaces history with a summary (X's model-side functionCall summarized away, tracking blanket-cleared) → the re-pushed body lands AFTER compression and survives to settle → this reconcile cannot pair X's call id against any resident functionCall, so X is left resident-but-untracked → the next Skill X invocation passes the dedup guard and injects a second full copy (one duplicate body — the documented self-healing direction, hence Suggestion). The strip-time gate was resolved at strip time precisely against this class; this reconcile re-derives via the severable pairing instead. — Suggested fix: when orphanStrippedSkillNames resolved at strip time, reconcile residency for exactly those names by body-content presence in the settled history (a residency signal compression cannot sever) instead of relying solely on call-id pairing — or document the residual here as the accepted trade-off.
中文说明
[建议] R12-5:settle 阶段的 reconcile 在落定时通过 call-id 配对重新推导驻留性,但 #sendMessageStreamWithAutoCompression 在 chat.sendMessageStream 推送续传内容(geminiChat.ts:2641)之前就先运行了 tryCompressChat(Session.ts:5837)。失败场景:ACP 续接一个孤儿尾部携带 skill X 正文的被中断轮 → strip 取消跟踪 X 并在 strip 时解析出名字 → 推送前的 LLM 摘要压缩把历史替换为摘要(X 的模型侧 functionCall 被摘要掉、跟踪被整体清空)→ 重推的正文在压缩之后才落盘并存活到 settle → 此 reconcile 无法把 X 的 call id 与任何驻留的 functionCall 配对,于是 X 变成"正文驻留但未被跟踪" → 下次调用 Skill X 穿过去重守卫、注入第二份完整正文(一份重复正文——文档化的自愈方向,因此定为建议级)。strip 时的门控正是为了防御这一类情况才在 strip 时解析名字;而这里的 reconcile 却改用了可以被压缩切断的配对来重新推导。建议修复:当 strip 时解析出了 orphanStrippedSkillNames 时,针对这些名字按落定历史中的正文内容存在性做驻留重建(这是压缩无法切断的驻留信号),而不是仅依赖 call-id 配对——或者在此处用注释明确声明该残余属于已接受的权衡。
— qwen3.8-max via Qwen Code /review (v0.21.11)
What this PR does
Keeps the loaded-skill bookkeeping in sync whenever conversation history rewrites remove skill bodies, and adds a user-facing
/unskill <name>command that releases a loaded skill's body from context on demand.Concretely: microcompaction now reports which skill bodies it blanked (and how many it could not resolve); every history-rewriting path — pre-send microcompaction,
/compress-fast, the memory-pressurecompact_historystep, and LLM/compress— consumes that information to un-track the affected skills; and/unskillreplaces a loaded skill's body with a short placeholder, adjusts the session token estimate downward, and un-tracks the skill so it reloads in full on its next invocation. Skill listing stays untouched — only the in-context body is evicted, and reloading is free: the nextSkillinvocation returns the full body again because the dedup guard is re-armed.The fallback direction is deliberate: when a blanked skill body cannot be resolved to a name, tracking is cleared wholesale rather than partially — over-clearing only costs one duplicated body on the next invoke, while under-clearing would leave a skill permanently unreloadable.
Why it's needed
Two related problems, reproduced on the current release:
activein/context, but the body is gone from the model's view. Worse, the dedup guard then answers every re-invocation with "already loaded in context", so the skill is permanently unusable for the rest of the session — the model keeps referring to instructions it can no longer see, and there is no way to recover other than starting a new session.Reviewer Test Plan
How to verify
This touches user-visible behavior; the most meaningful check is a short interactive run against a built bundle (
npm run build && npm run bundle, thennode dist/cli.js) with any test skill installed (e.g. a smalldemo-poemskill under~/.qwen/skills/).Eviction sync (the bug fix):
QWEN_MC_KEEP_RECENT=1(the keep-recent budget is clamped to ≥1, so a newer tool result must exist to push the skill body out of the protected window)./context detailshows it asactivewith abody loadedtoken line.echo hi) so a newer tool result exists./compress-fast. Expected: the skill body is among the blanked results./context detailagain. Expected (fixed behavior): the skill no longer showsactive. (Before this PR, it kept showingactive— the ghost state.)Skilltool returns the full body and/contextshowsactiveagain. (Before this PR, the tool returned "already loaded in context" forever.)/unskill(the new command):activein/context detailand note thebody loadedtoken line./unskill <name>. Expected: an info messageUnloaded skill "<name>" (~N tokens freed). Invoke it again to reload./context detailno longer marks the skill active.activerestored — a closed state loop./unskill nonexistentprintsSkill "nonexistent" is not loaded in context.;/unskillwith no argument prints usage; tab completion offers only currently loaded skill names.Caution while verifying: don't ask the model to recite the skill body before eviction — the recitation lands in a model message that compaction does not blank, which would invalidate a "can the model still see the body?" probe. Assert on
/contextand the raw re-invoke tool result instead.Unit-level: new tests cover the eviction reporting (resolved vs unresolved skill names), the sync consumer at all four call sites (targeted unload vs wholesale clear), the
/unskillcore operation (placeholder replacement, token adjustment, no-op on unknown names), and the command itself (not-loaded message, happy path, completion). Regression suites for chat, client, memory-pressure monitoring, microcompaction, and the skill tool all pass unchanged.Evidence (Before & After)
Before (release build, tmux run): after
/compress-fastblanked the skill body,/context detailstill showeddemo-poem active, the model could no longer see the body, and re-invoking the skill returnedSkill "demo-poem" is already loaded in context.— a dead end.After (this branch, tmux run):
Eviction path after the fix:
/compress-fastfreed ~299 tokens including the skill body;/context detailimmediately stopped showingactive; re-invoking returned the full body instead of "already loaded in context".Tested on
Environment (optional)
Built bundle (
npm run build && npm run bundle,node dist/cli.js) in tmux sessions; the four-step eviction sequence,/unskilllifecycle, and edge cases verified live. Unit and regression suites via vitest.Risk & Scope
--all/--listflags; no cross-session restore of loaded state on resume; subagent skill loading is untouched.Linked Issues
Refs #6762 — intentionally not auto-closing: #6762 is a five-mechanism umbrella request (model-invocable unskill tool, COMPACTABLE_TOOLS — already merged as #6788, TTL/turn auto-evict,
lifecycle: one-shotfrontmatter,<loaded_skills>system-prompt section). This PR delivers the eviction-state sync plus the user-only half of mechanism 1; the remaining mechanisms stay tracked on #6762 for follow-up PRs.中文说明
这个 PR 做了什么
让"已加载 skill"的簿记在会话历史被改写、skill 正文被移除时保持同步,并新增面向用户的
/unskill <name>命令,可按需把已加载 skill 的正文从上下文中释放。具体来说:微压缩现在会上报它清掉了哪些 skill 正文(以及有多少无法解析);所有改写历史的路径——发送前微压缩、
/compress-fast、内存压力的compact_history步骤、LLM/compress——都会消费这些信息来取消对应 skill 的"已加载"标记;/unskill会把已加载 skill 的正文替换为一条简短的占位符、下调会话 token 估算、并取消该 skill 的标记,使其在下一次调用时完整重载。skill 的列表完全不受影响——只有上下文中的正文被驱逐;重载是零成本的:下一次Skill调用会返回完整正文,因为去重守卫被重新激活。降级方向是刻意选择的:当被清空的 skill 正文无法解析出名字时,整体清空标记而不是部分清理——多清的代价只是下次调用时重复附加一份正文,而少清会让 skill 永久无法重载。
为什么需要
两个相关问题,均已在当前发布版上复现:
/context中仍显示active,但模型已经看不到正文。更糟的是,去重守卫此后对每次重新调用都返回 "already loaded in context",该 skill 在本会话中永久失效——模型持续引用它看不到的指令,除了开新会话别无恢复手段。评审者测试计划
如何验证
此改动涉及用户可见行为,最有意义的检查是拿构建产物做一小段交互式运行(
npm run build && npm run bundle,然后node dist/cli.js),并准备任意测试 skill(例如在~/.qwen/skills/下放一个小型demo-poemskill)。驱逐同步(bug 修复):
QWEN_MC_KEEP_RECENT=1启动 CLI(keep-recent 预算被钳位到 ≥1,因此需要一条更新的工具结果才能把 skill 正文挤出保护窗口)。/context detail显示其为active并带body loadedtoken 行。echo hi),让更新的工具结果存在。/compress-fast。预期:skill 正文在被清空的条目之中。/context detail。预期(修复后行为):skill 不再显示active。(本 PR 之前,它会一直显示active——幽灵状态。)Skill工具返回完整正文,/context重新显示active。(本 PR 之前,工具永远返回 "already loaded in context"。)/unskill(新命令):/context detail中确认active并记下body loadedtoken 行。/unskill <name>。预期:info 消息Unloaded skill "<name>" (~N tokens freed). Invoke it again to reload./context detail不再标记该 skill 为 active。active恢复——状态闭环。/unskill nonexistent打印Skill "nonexistent" is not loaded in context.;不带参数的/unskill打印用法;tab 补全只列出当前已加载的 skill 名。验证时注意:不要让模型在驱逐前复述 skill 正文——复述会落在压缩不会清空的模型消息里,使"模型是否还能看到正文"的探测失效。应断言
/context输出和重新调用的原始工具结果。单测层面:新增测试覆盖驱逐上报(可解析与不可解析的 skill 名)、四个调用点上的同步消费者(定向卸载 vs 整体清空)、
/unskill的核心操作(占位符替换、token 调整、未知名字的 no-op)以及命令本身(未加载提示、正常路径、补全)。chat、client、内存压力监控、微压缩、skill 工具的回归套件全部原样通过。证据(前后对比)
修复前(发布版构建,tmux 实录):
/compress-fast清空 skill 正文后,/context detail仍显示demo-poem active,模型已看不到正文,且重新调用 skill 返回Skill "demo-poem" is already loaded in context.——死路一条。修复后(本分支,tmux 实录):
修复后的驱逐路径:
/compress-fast释放了约 299 tokens(含 skill 正文);/context detail立即停止显示active;重新调用返回完整正文而非 "already loaded in context"。测试平台
环境(可选)
构建产物(
npm run build && npm run bundle,node dist/cli.js)在 tmux 会话中实测:四步驱逐序列、/unskill生命周期及边界用例均已现场验证。单测与回归套件通过 vitest 运行。风险与范围
--all/--list标志;resume 时不恢复已加载状态;子代理的 skill 加载不受影响。关联 Issue
Refs #6762 —— 有意不自动关闭:#6762 是一个五机制的组合需求(模型可调用的 unskill 工具、COMPACTABLE_TOOLS——已作为 #6788 合入、TTL/轮次自动驱逐、
lifecycle: one-shotfrontmatter、<loaded_skills>系统提示段)。本 PR 交付的是驱逐状态同步 + 机制 1 的用户侧部分;其余机制继续由 #6762 跟踪,留待后续 PR。