Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dd5e93c
fix(core): sync loaded-skill state with history eviction; add user /u…
Aug 10, 2026
6be1366
fix(core): address #8900 review — kept-suppression, hook dedup, i18n
Aug 11, 2026
68fad4c
fix(core): widen hookConfigKey to HookConfig so tsc --build passes
Aug 11, 2026
f87e889
fix(core): address R2 review — positive body check, rewind sync, dedup
Aug 11, 2026
19b901b
fix(core): address R3 review — sync-before-disarm, body guard, i18n p…
Aug 11, 2026
64a6a3e
fix(test): use buildSkillLlmContent in skill-eviction test fixtures
Aug 11, 2026
7b1dd42
fix(core): R4-1 — unloadSkillBody positive body check
Aug 12, 2026
76006c4
fix(core): move clearLoadedSkillTracking to GeminiChat layer
Aug 12, 2026
3cb3cf7
chore: merge main into feat/issue-6762-unskill
qwen-code-dev-bot Aug 12, 2026
f61a2f7
fix(core): guard chat-layer skill tracking clears with change checks
Aug 13, 2026
ac0c8c3
Merge remote-tracking branch 'origin/main' into feat/issue-6762-unskill
Aug 13, 2026
0ea2487
fix(core): keep loaded-skill tracking consistent across forked chats …
Aug 13, 2026
14df6c0
fix(core): reconcile loaded-skill tracking after history rewrites
Aug 13, 2026
d3fb5b0
fix(core): remove duplicate mock property breaking tsc --build
Aug 13, 2026
683169d
fix(core): harden loaded-skill tracking per R10 review
Aug 14, 2026
e7b7b01
fix(core): residency-aware settle reconcile and unskill pre-init guard
Aug 14, 2026
4cfe864
fix(core): harden skill-body residency check; document settle trade-off
Aug 17, 2026
8cbfecd
Merge origin/main into feat/issue-6762-unskill
Aug 18, 2026
de794a6
Merge remote-tracking branch 'origin/main' into feat/issue-6762-unskill
Aug 18, 2026
94ddbad
fix(core): blanket-clear tracking when a stripped skill body is unres…
Aug 18, 2026
03988a3
fix(core): close unresolvable-body gaps in strip sync and /unskill
Aug 18, 2026
f03e9f5
chore: drop stray local files accidentally carried in PR diff
Aug 18, 2026
000f5fe
fix(core): broaden /unskill refusal to any unattributable skill result
Aug 18, 2026
58c7a63
fix(core): scope /unskill markers to Skill responses; never enlarge t…
Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ describe('Session', () => {
truncateHistory: vi.fn(),
stripThoughtsFromHistory: vi.fn(),
stripOrphanedUserEntriesFromHistory: vi.fn().mockReturnValue([]),
reconcileLoadedSkillTracking: vi.fn(),
setTools: vi.fn(),
} as unknown as GeminiChat;
mockGeminiClient = {
Expand Down Expand Up @@ -2310,6 +2311,9 @@ describe('Session', () => {
mockChat.getHistory = vi
.fn()
.mockReturnValue([{ role: 'user', parts: [{ text: 'unanswered' }] }]);
mockChat.stripOrphanedUserEntriesFromHistory = vi
.fn()
.mockReturnValue([{ role: 'user', parts: [{ text: 'unanswered' }] }]);
// Force the continuation send to fail NON-cancelled (session token limit)
// so it hits the `!responseStream` branch — the data-loss window.
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
Expand Down Expand Up @@ -2338,6 +2342,9 @@ describe('Session', () => {
]),
}),
);
// The strip un-tracked any skill body it removed; once the orphan is
// preserved back, tracking must be rebuilt from the settled history.
expect(mockChat.reconcileLoadedSkillTracking).toHaveBeenCalled();
});

it('restores the orphaned turn when a continuation send throws (no data loss)', async () => {
Expand Down Expand Up @@ -2375,6 +2382,7 @@ describe('Session', () => {
]),
}),
);
expect(mockChat.reconcileLoadedSkillTracking).toHaveBeenCalled();
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
});

it('rejects (accepted:false) when a prompt is already in flight', async () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4000,6 +4000,12 @@ export class Session implements SessionContext {
// — so hold it (and a push-count snapshot) to restore on that path.
let strippedOrphanEntries: Content[] | null = null;
let orphanPushCountSnapshot = 0;
// The continuation strip un-tracks any skill body it removes;
// once the continuation settles (re-push landed, or the catch
// block restored the orphan) residency is knowable again and
// tracking must be reconciled — this path has no client.ts-style
// restore hook to re-track through.
let orphanStrippedForContinuation = false;
if (goalTurn?.origin === 'runtime') {
this.config.getChatRecordingService()?.recordGoalRuntimeMessage(
modelPromptBlocks
Expand Down Expand Up @@ -4035,6 +4041,8 @@ export class Session implements SessionContext {
strippedOrphanEntries =
this.#getCurrentChat().stripOrphanedUserEntriesFromHistory() ??
null;
orphanStrippedForContinuation =
(strippedOrphanEntries?.length ?? 0) > 0;
orphanPushCountSnapshot =
this.#getCurrentChat().getUserContentPushCount?.() ?? 0;
continuationParts = recoveryPlan.continuation.parts;
Expand Down Expand Up @@ -4521,6 +4529,13 @@ export class Session implements SessionContext {
// turn proceeds, on every exit: normal end-of-stream,
// cancellation returns, and thrown stream errors alike.
await messageDisplay?.finish();
// Every exit path leaves history in a final state (send
// re-pushed the continuation, or catch restored the
// orphan), so rebuild loaded-skill tracking from it.
if (orphanStrippedForContinuation) {
orphanStrippedForContinuation = false;
this.#getCurrentChat().reconcileLoadedSkillTracking();
}
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
}

commitChannelDeliveryResponseBlock(
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/i18n/locales/ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -2836,4 +2836,19 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
"Els canvis del gestor d'habilitats automàtiques només estan disponibles en espais de treball de confiança. Marca aquesta carpeta com a fiable amb `/trust` i torna-ho a provar.",
'Kept model as {{model}}': 'Model mantingut com a {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'Descarrega del context el cos d’una habilitat carregada, alliberant-ne els tokens per a la resta de la sessió (a costa d’un reompliment de la memòria cau de prompt). L’habilitat continua disponible i es recarrega completament en la propera invocació.',
'Usage: /unskill <skill-name>': 'Ús: /unskill <skill-name>',
'Could not retrieve skill manager.':
'No s’ha pogut obtenir el gestor d’habilitats.',
'Skill "{{name}}" is not loaded in context.':
'L’habilitat "{{name}}" no està carregada al context.',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'L’habilitat "{{name}}" ja no tenia cos al context; s’ha esborrat el seguiment perquè es pugui recarregar.',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'Habilitat "{{name}}" descarregada (~{{tokens}} tokens alliberats). Invoca-la de nou per recarregar-la.',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" no és una habilitat (pot ser una comanda invocable pel model); /unskill només descarrega cossos d’habilitats.',
};
15 changes: 15 additions & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -2317,4 +2317,19 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Änderungen durch den Auto-Skill-Kurator sind nur in vertrauenswürdigen Arbeitsbereichen verfügbar. Stufen Sie diesen Ordner mit `/trust` als vertrauenswürdig ein und versuchen Sie es erneut.',
'Kept model as {{model}}': 'Modell als {{model}} beibehalten',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'Entlädt den Body eines geladenen Skills aus dem Kontext und gibt dessen Tokens für den Rest der Sitzung frei (kostet eine erneute Befüllung des Prompt-Caches). Der Skill bleibt verfügbar und wird beim nächsten Aufruf vollständig neu geladen.',
'Usage: /unskill <skill-name>': 'Verwendung: /unskill <skill-name>',
'Could not retrieve skill manager.':
'Skill-Manager konnte nicht abgerufen werden.',
'Skill "{{name}}" is not loaded in context.':
'Skill "{{name}}" ist nicht im Kontext geladen.',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'Skill "{{name}}" hatte keinen Body mehr im Kontext; die Nachverfolgung wurde zurückgesetzt, sodass er neu geladen werden kann.',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'Skill "{{name}}" entladen (~{{tokens}} Tokens freigegeben). Rufen Sie ihn erneut auf, um ihn neu zu laden.',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" ist kein Skill (möglicherweise ein vom Modell aufrufbarer Befehl); /unskill entlädt nur Skill-Bodys.',
};
14 changes: 14 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2828,4 +2828,18 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.',
'Kept model as {{model}}': 'Kept model as {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.',
Comment thread
ZijianZhang989 marked this conversation as resolved.
Resolved
'Usage: /unskill <skill-name>': 'Usage: /unskill <skill-name>',
'Could not retrieve skill manager.': 'Could not retrieve skill manager.',
'Skill "{{name}}" is not loaded in context.':
'Skill "{{name}}" is not loaded in context.',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.',
};
15 changes: 15 additions & 0 deletions packages/cli/src/i18n/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -2322,4 +2322,19 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Les modifications du gestionnaire de compétences automatiques ne sont disponibles que dans les espaces de travail approuvés. Marquez ce dossier comme approuvé avec `/trust`, puis réessayez.',
'Kept model as {{model}}': 'Modèle conservé : {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'Décharge du contexte le corps d’une compétence chargée, libérant ses tokens pour le reste de la session (au prix d’un nouveau remplissage du cache d’invite). La compétence reste disponible et se recharge intégralement à sa prochaine invocation.',
'Usage: /unskill <skill-name>': 'Utilisation : /unskill <skill-name>',
'Could not retrieve skill manager.':
'Impossible de récupérer le gestionnaire de compétences.',
'Skill "{{name}}" is not loaded in context.':
'La compétence "{{name}}" n’est pas chargée dans le contexte.',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'La compétence "{{name}}" n’avait plus de corps dans le contexte ; le suivi a été réinitialisé pour permettre son rechargement.',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'Compétence "{{name}}" déchargée (~{{tokens}} tokens libérés). Invoquez-la à nouveau pour la recharger.',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" n’est pas une compétence (il peut s’agir d’une commande invocable par le modèle) ; /unskill ne décharge que les corps de compétences.',
};
15 changes: 15 additions & 0 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -2083,4 +2083,19 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'自動スキル管理による変更は信頼済みのワークスペースでのみ利用できます。`/trust` でこのフォルダーを信頼してから、もう一度お試しください。',
'Kept model as {{model}}': 'モデルは {{model}} のままです',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'読み込み済みスキルの本文をコンテキストからアンロードし、このセッションの残りで使えるトークンを解放します(プロンプトキャッシュの再充填が1回発生します)。スキルは利用可能なまま保持され、次回呼び出し時に全文が再読み込みされます。',
'Usage: /unskill <skill-name>': '使用法: /unskill <skill-name>',
'Could not retrieve skill manager.':
'スキルマネージャーを取得できませんでした。',
'Skill "{{name}}" is not loaded in context.':
'スキル "{{name}}" はコンテキストに読み込まれていません。',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'スキル "{{name}}" の本文はコンテキストに残っていません。追跡状態をクリアしたので再読み込みできます。',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'スキル "{{name}}" をアンロードしました(約 {{tokens}} トークンを解放)。再度呼び出すと再読み込みされます。',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" はスキルではありません(モデルが呼び出せるコマンドの可能性があります)。/unskill はスキル本文のみをアンロードします。',
};
15 changes: 15 additions & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2301,4 +2301,19 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'As alterações do gerenciador de habilidades automáticas estão disponíveis apenas em espaços de trabalho confiáveis. Marque esta pasta como confiável usando `/trust` e tente novamente.',
'Kept model as {{model}}': 'Modelo mantido como {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'Descarrega do contexto o corpo de uma habilidade carregada, liberando seus tokens para o restante da sessão (ao custo de um reabastecimento do cache de prompt). A habilidade permanece disponível e é recarregada por completo na próxima invocação.',
'Usage: /unskill <skill-name>': 'Uso: /unskill <skill-name>',
'Could not retrieve skill manager.':
'Não foi possível obter o gerenciador de habilidades.',
'Skill "{{name}}" is not loaded in context.':
'A habilidade "{{name}}" não está carregada no contexto.',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'A habilidade "{{name}}" não tinha mais corpo no contexto; o rastreamento foi limpo para que ela possa ser recarregada.',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'Habilidade "{{name}}" descarregada (~{{tokens}} tokens liberados). Invoque-a novamente para recarregar.',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" não é uma habilidade (pode ser um comando invocável pelo modelo); /unskill descarrega apenas corpos de habilidades.',
};
14 changes: 14 additions & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -2292,4 +2292,18 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Изменения куратора автоматических навыков доступны только в доверенных рабочих пространствах. Сделайте эту папку доверенной с помощью `/trust` и повторите попытку.',
'Kept model as {{model}}': 'Оставлена модель {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'Выгружает тело загруженного навыка из контекста, освобождая его токены для остальной части сессии (ценой одного повторного заполнения кеша промптов). Навык остаётся доступным и полностью перезагружается при следующем вызове.',
'Usage: /unskill <skill-name>': 'Использование: /unskill <skill-name>',
'Could not retrieve skill manager.': 'Не удалось получить менеджер навыков.',
'Skill "{{name}}" is not loaded in context.':
'Навык "{{name}}" не загружен в контекст.',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'Тела навыка "{{name}}" больше нет в контексте; отслеживание сброшено, и его можно перезагрузить.',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'Навык "{{name}}" выгружен (освобождено ~{{tokens}} токенов). Вызовите его снова для перезагрузки.',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" — не навык (возможно, это вызываемая моделью команда); /unskill выгружает только тела навыков.',
};
14 changes: 14 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -2407,4 +2407,18 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'只有受信任的工作區可以變更自動技能管理器。請透過 `/trust` 信任此資料夾後再試一次。',
'Kept model as {{model}}': '模型保持為 {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'從上下文卸載已載入的技能主體,為本次工作階段的剩餘部分釋出其 token(代價是一次提示快取重新填充)。技能保持可用,下次呼叫時會完整重新載入。',
'Usage: /unskill <skill-name>': '用法:/unskill <skill-name>',
'Could not retrieve skill manager.': '無法取得技能管理器。',
'Skill "{{name}}" is not loaded in context.':
'技能 "{{name}}" 未載入上下文。',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'技能 "{{name}}" 在上下文中已無主體;已清除追蹤狀態,可重新載入。',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'已卸載技能 "{{name}}"(釋放約 {{tokens}} 個 token)。再次呼叫即可重新載入。',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" 不是技能(可能是模型可呼叫的命令);/unskill 只能卸載技能主體。',
};
14 changes: 14 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -2609,4 +2609,18 @@ export default {
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'仅受信任的工作区可以更改自动技能管理器。请通过 `/trust` 信任此文件夹后重试。',
'Kept model as {{model}}': '模型保持为 {{model}}',

// /unskill command
'Unload a loaded skill body from context, freeing its tokens for the rest of the session (costs one prompt-cache re-fill). The skill stays available and reloads in full on its next invocation.':
'从上下文卸载已加载的技能正文,为本次会话的剩余部分释放其 token(代价是一次提示缓存重新填充)。技能保持可用,下次调用时会完整重新加载。',
'Usage: /unskill <skill-name>': '用法:/unskill <skill-name>',
'Could not retrieve skill manager.': '无法获取技能管理器。',
'Skill "{{name}}" is not loaded in context.':
'技能 "{{name}}" 未加载到上下文中。',
'Skill "{{name}}" had no body left in context; tracking cleared so it can be reloaded.':
'技能 "{{name}}" 在上下文中已无正文;已清除跟踪状态,可重新加载。',
'Unloaded skill "{{name}}" (~{{tokens}} tokens freed). Invoke it again to reload.':
'已卸载技能 "{{name}}"(释放约 {{tokens}} 个 token)。再次调用即可重新加载。',
'"{{name}}" is not a skill (it may be a model-invocable command); /unskill only unloads skill bodies.':
'"{{name}}" 不是技能(可能是模型可调用的命令);/unskill 只能卸载技能正文。',
};
Loading
Loading