Refonte du seuil d’auto-compaction
Statut : Brouillon · 2026-05-14
Contexte
Cette section décrit l’état avant l’implémentation de cette PR (comportement pré-refonte). Les références suivantes —
COMPRESSION_TOKEN_THRESHOLD,thinkingConfig.includeThoughts = true,hasFailedCompressionAttempt, et les citations fichier:ligne — correspondent au code avant la fusion de la PR #4345. Après fusion, ces symboles/numéros de ligne ne seront plus valables.
L’auto-compression actuelle de qwen-code utilise un seul seuil proportionnel COMPRESSION_TOKEN_THRESHOLD = 0.7 (chatCompressionService.ts:33), partagé par toutes les tailles de fenêtre. Par rapport à l’« échelle de jetons absolue » de claude-code (autoCompact.ts:62-65), qwen-code présente trois problèmes concrets :
-
Réservation excessive dans les grandes fenêtres : pour un modèle 1M, le seuil à 70 % se déclenche à 700K, laissant 300K restants, bien au-delà des ~33K réellement nécessaires pour le résumé + la sortie.
-
Verrouillage permanent après 1 échec : une fois
hasFailedCompressionAttempt = true, le session entier n’essaie plus l’auto-compact (geminiChat.ts:504), plus strict que le « fusible 3 échecs consécutifs » de claude-code. -
Découplage du système de tips et du seuil auto : les trois tips
context-*danstipRegistry.tsutilisent des pourcentages fixes 50/80/95%, totalement indépendants du seuil auto (70 %). Cela implique que les tips à 80 % / 95 % se déclenchent très rarement sur le chemin normal (auto fonctionne), mais sur les chemins périphériques (échec auto / rattrapage réactif) ils manquent de sémantique alignée avec le seuil. -
L’appel de compression lui-même n’a pas de contrôle de budget de sortie : chatCompressionService.ts:374-376 active explicitement
thinkingConfig.includeThoughts = true(commentaire : « Compression quality drives every subsequent main turn »), tandis que l’appel sideQuery n’a pas de limitemaxOutputTokens. Le commentaire du code (:436-437) reconnaît également quecompressionOutputTokenCount may include non-persisted tokens (thoughts). Lorsque la compression approche du sommet de la fenêtre, la sortie totale peut gonfler, rendant la réservation du buffer imprévisible.Pire encore, le comportement varie selon les fournisseurs : le thinking budget d’Anthropic est totalement indépendant de max_tokens ; les reasoning tokens d’OpenAI ne sont pas limités par max_completion_tokens ; le comportement de Gemini varie selon la version du modèle. Cela signifie qu’« ajouter simplement maxOutputTokens pour contrôler la sortie totale » ne fonctionne pas dans un projet multi-fournisseurs comme qwen-code.
-
Le jugement du seuil utilise
lastPromptTokenCount, systématiquement sous-estimé. geminiChat.ts:1217-1232 montre que cette valeur provient deusageMetadata.totalTokenCountde la réponse API précédente. Deux écarts : (a) elle n’inclut pas le message utilisateur qui sera ajouté dans ce tour-ci, donc chaque vérification cheap-gate traite un prompt plus petit que la réalité ; (b) la valeur initiale est 0, donc la première envoi avec--continuerestaurer un large historique / sub-agent héritant de beaucoup d’historique ignore tous les seuils. Comparé àtokenCountWithEstimationde claude-code (query.ts:638) qui suit une approche double piste (dernier usage assistant API + estimation des messages ajoutés après), ce qui ferme ces deux écarts.
Objectifs de conception
- Introduire un seuil hybride « proportionnel + absolu », permettant aux grands modèles fenêtrés d’être régis par la valeur absolue, tandis que les petites fenêtres conservent une sécurité proportionnelle.
- Ajouter deux niveaux warn / hard (auto restant le point de déclenchement principal), formant une échelle à trois niveaux.
- Réécrire le système de tips pour qu’ils suivent les nouveaux seuils.
- Faire passer la gestion des échecs de « 1 échec → verrouillage permanent » à « fusible de 3 échecs + rétablissement automatique ».
- Fermer le thinking dans l’appel de compression et ajouter une limite
maxOutputTokens: alignement avec claude-code, pour que la sortie totale soit contrainte par un seul paramètre et que le budget du buffer soit prévisible ; accepter une possible dégradation de la qualité de compression. - Ajouter une compensation d’estimation de tokens : éliminer les deux biais systématiques de
lastPromptTokenCount(« décalage d’un tour » et « initialement 0 ») pour que le jugement du seuil soit plus proche de la taille réelle du prompt. - Supprimer l’entrée de configuration
contextPercentageThresholddes paramètres (la constante PCT interne est conservée). - Ne pas introduire de canal de surcharge via variable d’environnement, ne pas ajouter de commutateur enabled explicite.
Échelle à trois niveaux de seuils
window (fenêtre de contexte brute)
│
│ ← SUMMARY_RESERVE = 20K
▼
effectiveWindow
│
│ ← HARD_BUFFER = 3K
▼
hard_threshold = effectiveWindow - 3K
│
│ ← (AUTOCOMPACT_BUFFER - HARD_BUFFER) = 10K
▼
auto_threshold = max(PCT * window, effectiveWindow - AUTOCOMPACT_BUFFER)
│
│ ← WARN_BUFFER = 20K
▼
warn_threshold = max((PCT - WARN_OFFSET) * window, auto_threshold - WARN_BUFFER)
│
▼
0Sémantique des trois niveaux
| Niveau | Condition de déclenchement | Comportement |
|---|---|---|
| warn | tokenCount >= warn_threshold | Indication UI « X tokens restants avant auto-compression », ne change pas le comportement d’envoi. |
| auto | tokenCount >= auto_threshold | Avant l’envoi, tryCompress(force=false), processus de compression normal. |
| hard | tokenCount >= hard_threshold | Avant l’envoi, tryCompress(force=true), réinitialise le verrou d’échec et force la compression. |
Le niveau hard équivaut à anticiper la logique de rattrapage réactive actuelle (geminiChat.ts:711) avant l’envoi, évitant ainsi un aller-retour coûteux avec une requête oversize ayant échoué.
Constantes internes
// chatCompressionService.ts
const DEFAULT_PCT = 0.7; // seuil proportionnel auto de repli
const WARN_PCT_OFFSET = 0.1; // seuil warn proportionnel = PCT - WARN_OFFSET = 0.6
const COMPACT_MAX_OUTPUT_TOKENS = 20_000; // limite dure de sortie pour la sideQuery de compression (thinking + résumé cumulés)
const SUMMARY_RESERVE = 20_000; // réservation de sortie soustraite du haut de la fenêtre = maxOutput
const AUTOCOMPACT_BUFFER = 13_000; // espacement entre auto et effectiveWindow
const WARN_BUFFER = 20_000; // espacement entre warn et auto
const HARD_BUFFER = 3_000; // espacement entre hard et effectiveWindow
const MAX_CONSECUTIVE_FAILURES = 3; // seuil de fusible d'échecValeurs issues : toutes reprises des mesures réelles de claude-code (autoCompact.ts:30,62-65).
SUMMARY_RESERVE = COMPACT_MAX_OUTPUT_TOKENS est une relation clé : le modèle est contraint par la limite dure maxOutputTokens, la sortie ne peut pas dépasser 20K, donc la réservation n’a pas besoin de marge de sécurité supplémentaire. Note : cette relation est vraie lorsque le thinking est désactivé (tout le budget de sortie va au résumé). Si le thinking est conservé, thinking + résumé partagent le budget (sémantique maxOutputTokens du SDK Gemini / de la plupart des fournisseurs), le modèle répartit entre les deux, donc l’espace réel disponible pour le résumé est inférieur à 20K (voir les risques et précautions, points 1 et 2).
Fonction de calcul
export interface CompactionThresholds {
warn: number;
auto: number;
hard: number; // lorsque hard < auto, égal à auto (dégradation petite fenêtre)
effectiveWindow: number;
}
export function computeThresholds(window: number): CompactionThresholds {
const effectiveWindow = window - SUMMARY_RESERVE;
const absAuto = effectiveWindow - AUTOCOMPACT_BUFFER;
const auto = Math.max(DEFAULT_PCT * window, absAuto);
const absWarn = auto - WARN_BUFFER;
const warn = Math.max((DEFAULT_PCT - WARN_PCT_OFFSET) * window, absWarn);
const rawHard = effectiveWindow - HARD_BUFFER;
const hard = Math.max(rawHard, auto); // sous dégradation pour les petites fenêtres
return { warn, auto, hard, effectiveWindow };
}Données mesurées
| Fenêtre | warn | auto | hard | Remarque |
|---|---|---|---|---|
| 32K | 19.2K (pct) | 22.4K (pct) | 22.4K (dég.) | Repli proportionnel |
| 64K | 38.4K (pct) | 44.8K (pct) | 44.8K (dég.) | Repli proportionnel |
| 128K | 76.8K (pct) | 95K (abs) | 105K (abs) | Hybride (warn=pct, auto/hard=abs) |
| 200K | 147K (abs) | 167K (abs) | 177K (abs) | Prise en charge absolue |
| 256K | 203K (abs) | 223K (abs) | 233K (abs) | Prise en charge absolue |
| 1M | 947K (abs) | 967K (abs) | 977K (abs) | Tout absolu |
(pct) signifie que le niveau est déterminé par la formule proportionnelle, (abs) par la formule absolue.
Configuration utilisateur
Modifications de ChatCompressionSettings
// packages/core/src/config/config.ts:217
export interface ChatCompressionSettings {
/** Conservé (sans rapport avec cette refonte, utilisé par compactionInputSlimming) */
imageTokenEstimate?: number;
}Suppression : le champ contextPercentageThreshold. Raisons :
- Avec la nouvelle formule, pour les fenêtres courantes (>= 128K), ce champ n’a quasiment aucun effet — la valeur absolue prend le relais.
- Sur les petites fenêtres, la configuration utilisateur pourrait paradoxalement déclencher la compression plus tôt, contraire à l’intuition d’économiser des tokens.
- claude-code n’expose pas ce champ, il n’existe pas de précédent de configuration similaire côté utilisateur.
Gestion du breaking change
Côté utilisateur : lors du démarrage, la Config détecte la présence de chatCompression.contextPercentageThreshold :
- écrit un avertissement sur stderr :
"chatCompression.contextPercentageThreshold a été supprimé et est désormais contrôlé par des seuils intégrés." - Ne pas générer d’erreur, ne pas bloquer le démarrage.
- La valeur du champ est ignorée.
Côté SDK (R5.4) : le champ hasFailedCompressionAttempt: boolean de CompressOptions est renommé en consecutiveFailures: number. Deux différences :
| Ancien champ | Nouveau champ | |
|---|---|---|
| Nom | hasFailedCompressionAttempt | consecutiveFailures |
| Type | boolean | number |
| Sém | true = auto-compact désactivé définitivement | >= MAX_CONSECUTIVE_FAILURES (par défaut 3) = désactivé temporairement jusqu’à un force réussi |
Seul GeminiChat.tryCompress est consommateur interne dans le dépôt, donc la migration interne est à faible risque ; mais @qwen-code/qwen-code-core est un package publié, CompressOptions est visible dans les .d.ts, donc le code des SDK clients appelant directement service.compress({ ..., hasFailedCompressionAttempt: true }) obtiendra une erreur de compilation TypeScript. Guide de migration : remplacer true par MAX_CONSECUTIVE_FAILURES (ou tout entier >= 3), false par 0. Si l’appelant maintient son propre compteur d’échecs, il peut directement le passer.
Compensation d’estimation de tokens
lastPromptTokenCount de qwen-code provient de usageMetadata.totalTokenCount de la réponse API précédente (geminiChat.ts:1217-1232). Cela entraîne :
- Décalage d’un tour : le cheap-gate utilise
lastPromptTokenCountpour juger, mais le prompt réel de cet envoi =lastPromptTokenCount+ message utilisateur de ce tour. Les tokens sous-estimés peuvent rendre le jugement du seuil faux-négatif. - Initialement 0 : la valeur initiale est 0, donc le premier envoi (même avec
--continueou héritage d’historique par sub-agent) ignore tous les seuils.
Introduction d’une fonction d’estimation locale légère estimatePromptTokens pour compenser ces deux lacunes avant l’envoi (cheap-gate / hard) :
// chatCompressionService.ts (ou nouveau fichier packages/core/src/services/tokenEstimation.ts)
const BYTES_PER_TOKEN = 4; // estimation générique char/4 (identique à claude-code)
const BYTES_PER_TOKEN_JSON = 2; // JSON / tool_call input plus dense
/**
* Estime le nombre de tokens d'un ensemble de Content, pour compenser
* le décalage des métadonnées d'usage API.
* Pour les images / documents, réutilise imageTokenEstimate existant (défaut 1600).
*/
export function estimateContentTokens(
contents: Content[],
imageTokenEstimate = DEFAULT_IMAGE_TOKEN_ESTIMATE,
): number {
// Réutilise estimateContentChars (compactionInputSlimming.ts), puis divise par bytesPerToken
// En interne, utilise BYTES_PER_TOKEN_JSON pour functionCall / functionResponse
// ...
}
/**
* Point d'entrée unifié pour le cheap-gate et le jugement hard.
* Chemin principal : lastPromptTokenCount précis + estimation du message utilisateur de ce tour
* Chemin initial : estimation complète de l'historique
*/
export function estimatePromptTokens(
history: Content[],
userMessage: Content,
lastPromptTokenCount: number,
): number {
if (lastPromptTokenCount > 0) {
return lastPromptTokenCount + estimateContentTokens([userMessage]);
}
return estimateContentTokens([...history, userMessage]);
}Points d’application :
- Au cheap-gate de
chatCompressionService.compress(): remplacer la sourceoriginalTokenCountparestimatePromptTokens(history, userMessage, lastPromptTokenCount). - À l’entrée
geminiChat.sendMessageStreampour le jugement hard (voir section suivante).
L’estimation ne sert qu’à déclencher plus tôt, pas à sauter le déclenchement. Comme char/4 est une estimation basse grossière, elle est sûre du côté faux-positif (mieux vaut compresser un peu trop tôt), mais peu fiable du côté faux-négatif.
Modifications de la chaîne de déclenchement
chatCompressionService.ts
-
Exporter
computeThresholdspour réutilisation par cheap-gate / UI / commandes. -
compress()cheap-gate (lignes 221-249) :if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !force) { return NOOP; } const { auto } = computeThresholds(contextLimit); const effectiveTokens = estimatePromptTokens( curatedHistory, userMessage, originalTokenCount, ); if (!force && effectiveTokens < auto) return NOOP; -
Appel
runSideQuerydanscompress()(lignes 356-380) : désactiver thinking + ajoutermaxOutputTokens:const summaryResult = await runSideQuery(config, { // ... config: { thinkingConfig: { includeThoughts: false }, // fermer le thinking (aligné avec claude-code) maxOutputTokens: COMPACT_MAX_OUTPUT_TOKENS, // limite dure 20K }, // ... });Ou simplement supprimer
thinkingConfiget laisser la valeur par défaut derunSideQuery(sideQuery.ts:118 par défautincludeThoughts: false) prendre le relais.Avec thinking désactivé,
maxOutputTokenscontraint directement la sortie totale (pas de problème de budget thinking séparé), etSUMMARY_RESERVE = maxOutput = 20Kest une relation propre et dure.Mettre également à jour le commentaire de chatCompressionService.ts:374-376, qui passe de « Compression quality drives every subsequent main turn — keep reasoning on » à une explication indiquant que, pour garantir une limite de sortie prévisible entre fournisseurs, on s’aligne sur la conception de claude-code.
Le commentaire token math (:436-437) « may include non-persisted tokens (thoughts) » peut également être nettoyé.
geminiChat.ts : entrée sendMessageStream (ligne 562)
// Avant : tryCompress(force=false)
// Après : utiliser l'estimation pour décider si hard est atteint, déterminer le flag force
const { hard } = computeThresholds(contextLimit);
const effectiveTokens = estimatePromptTokens(
this.getHistory(true),
createUserContent(params.message),
this.lastPromptTokenCount,
);
const shouldForceFromHard = effectiveTokens >= hard;
if (shouldForceFromHard) {
// Réinitialise le fusible, équivalent à force compress
this.consecutiveFailures = 0;
}
compressionInfo = await this.tryCompress(
prompt_id,
model,
shouldForceFromHard,
params.config?.abortSignal,
);Gestion des échecs améliorée (geminiChat.ts:504-510)
// Avant
hasFailedCompressionAttempt: boolean;
// Après
consecutiveFailures: number; // par défaut 0
// Branche échec
} else if (isCompressionFailureStatus(info.compressionStatus)) {
if (!force) {
this.consecutiveFailures += 1;
}
}
// Branche succès
this.consecutiveFailures = 0;Un échec lors d’un appel force=true n’est pas compté (maintien de la sémantique actuelle : le réactif / manuel ne « consomme » pas de droits).
Modifications UI
Réécriture des trois tips context-* dans tipRegistry.ts
Les trois niveaux de seuils correspondent exactement aux trois tips. Correspondance (par ordre croissant de tokens) :
| ID du tip | Condition actuelle | Nouvelle condition | Changement de texte |
|---|---|---|---|
compress-intro | pct >= 50 && < 80 && sessionPromptCount > 5 | tokenCount >= warn && tokenCount < auto && sessionPromptCount > 5 | Inchangé |
context-high | pct >= 80 && < 95 | tokenCount >= auto && tokenCount < hard | Inchangé |
context-critical | pct >= 95 | tokenCount >= hard | Ajouter « L’auto-compact forcera au prochain envoi. » pour refléter le nouveau comportement du niveau hard. |
Impact sur la fréquence de déclenchement :
- Chemin principal (auto fonctionne) :
tokenCountdépasse auto puis déclenche immédiatement la compression, letokenCountdu tour suivant retombe, donccontext-highn’est visible que brièvement entre le déclenchement et l’effet de la compression. - Chemin périphérique (échec auto / fusible / réactif pas assez rapide) :
tokenCountcontinue d’augmenter, traversant successivement warn → auto → hard, déclenchant les trois tips, cohérent avec la perception utilisateur d’un contexte de plus en plus serré. - Quand
context-criticalse déclenche, le niveau hard a déjà forcé la compression avant envoi (spécification de la section modifications chaîne de déclenchement), donc ce tip est en réalité une « notification post-sauvetage » plutôt qu’un « avertissement pré-sauvetage ». Le texte ajoute une phrase explicative.
L’interface TipContext est enrichie :
export interface TipContext {
lastPromptTokenCount: number;
contextWindowSize: number;
sessionPromptCount: number;
sessionCount: number;
platform: string;
// Nouveau : permet à la fonction isRelevant d'accéder aux seuils.
// computeThresholds est calculé par l'appelant et injecté, pour éviter une dépendance directe de tipRegistry sur le core.
thresholds?: CompactionThresholds;
}AppContainer.tsx:1150 construit TipContext en synchronisation.
Synchronisation de la commande /context (contextCommand.ts:177-183)
// Remplacer la valeur codée en dur (1 - threshold) * contextWindowSize
const { warn, auto, hard, effectiveWindow } =
computeThresholds(contextWindowSize);
// Afficher quatre lignes :
// Fenêtre effective : 180K (fenêtre − 20K réserve)
// Seuil warn : 147K (...)
// Seuil auto : 167K ← position actuelle
// Seuil hard : 177K
// Marquer dans quel tier se situe le tokenCount actuelIndication continue dans le footer (optionnel, follow-up)
Cette spécification n’impose pas d’indication continue dans le footer. Raisons :
- Le système de tips existant peut déjà donner des indications dans l’historique.
- Une indication continue nécessite de modifier le rendu ink et augmente la fréquence de rafraîchissement.
- Cela peut être traité en follow-up après cette spécification (PR indépendante).
Si réalisée plus tard, la condition suggérée est tokenCount >= warn && tokenCount < auto ; après dépassement de auto, masquer (la compression a commencé).
Couverture des tests
Tests unitaires (chatCompressionService.test.ts)
computeThresholds(32K)→ branche repli proportionnel (warn/auto tous deux pct, hard dégradé).computeThresholds(128K)→ branche hybride (warn=pct, auto=abs, hard=abs).computeThresholds(200K)→ branche prise en charge absolue (warn/auto/hard tous abs).computeThresholds(1M)→ branche tout absolu.computeThresholds(window=10K)→ fenêtre très petite (absolu négatif), la formule ne plante pas.- Les trois seuils respectent toujours
warn <= auto <= hard. - Les formules max() sont stables au point limite (pct * window == abs).
Tests unitaires (tokenEstimation.test.ts)
estimateContentTokenspour texte pur / json / functionCall / functionResponse / image / document, chacun avec le bytesPerToken correspondant.estimatePromptTokensenlastPromptTokenCount > 0suit le « chemin principal », à 0 suit le « chemin initial ».- Un grand message utilisateur ajouté au cheap-gate permet de franchir le seuil auto.
- L’écart entre l’estimation et l’usage API réel est inférieur à ±30 % (validé avec des échantillons d’historique réels).
Tests d’intégration (geminiChat.test.ts / chatCompressionService.test.ts)
- Après 3 échecs consécutifs, cheap-gate NOOP ; le prochain
forcerétablit. - Un seul échec ne verrouille plus définitivement.
- Le dépassement du seuil hard estimé déclenche automatiquement un force compress avant envoi.
- L’appel sideQuery de compression transmet correctement
maxOutputTokens = COMPACT_MAX_OUTPUT_TOKENSàrunSideQuery, etthinkingConfig.includeThoughtsestfalse(ou pris en charge par la valeur par défaut de sideQuery). - Couverture du premier envoi : créer un chat avec
lastPromptTokenCount = 0mais un historique volumineux (simulant un--continue), le premier envoi déclenche le seuil auto via l’estimation.
Tests de compatibilité
- Démarrer avec
contextPercentageThreshold = 0.5→ avertissement stderr + champ ignoré, le comportement utilise la constante PCT interne.
Tests du système de tips (tipRegistry.test.ts)
- Les trois tips context-* se déclenchent correctement au franchissement de warn/auto/hard, sans chevauchement.
- Sur le chemin principal, après déclenchement du seuil auto et compression,
context-highn’est plus visible. - Sur le chemin périphérique (fusible + token continuant d’augmenter), les trois tips se déclenchent successivement.
- En l’absence de
thresholdsdansTipContext(fallback), le comportement est raisonnable.
Implémentation par phases
| Phase | Contenu | Indépendance |
|---|---|---|
| 1 | Constantes internes + computeThresholds + modifications cheap-gate (sans compensation d’estimation) | Fusionnable indépendamment |
| 2 | Amélioration de la gestion des échecs (1 → 3 fusible) | Fusionnable indépendamment |
| 3 | Anticipation du force compress du niveau hard | Dépend de P1 + P7 |
| 4 | Modifications configuration + avertissement breaking change | Dépend de P1 |
| 5 | UI (réécriture tips + /context) | Dépend de P1 |
| 6 | Fermeture thinking + ajout maxOutputTokens dans l’appel de compression sideQuery | Indépendant, peut précéder P1 |
| 7 | Compensation d’estimation de tokens (estimateContentTokens + estimatePromptTokens, appliqué à cheap-gate / hard) | Indépendant, peut être parallèle à P1 |
Chaque phase peut faire l’objet d’une PR indépendante. Ordre suggéré de fusion : P6 → P7 → P1 → P2 → P4 → P3 → P5 : d’abord ajouter la limite maxOutputTokens à l’appel de compression (pour que les hypothèses du buffer soient fiables) ; puis ajouter la compensation d’estimation (pour que le jugement sur les tokens soit plus fiable) ; ensuite mettre en place l’infrastructure des seuils ; puis le fusible d’échec, les modifications de configuration ; enfin activer le rattrapage actif du niveau hard (à ce moment, on dispose d’un compteur de tokens fiable et d’un fusible). Chaque PR peut être vérifiée et annulée indépendamment.
Risques et précautions
-
La désactivation du thinking peut dégrader la qualité du résumé. Le commentaire original « Compression quality drives every subsequent main turn — keep reasoning on » exprimait cette inquiétude. Le jugement de cette spécification est que la « limite de tokens prévisible » prime sur la « qualité maximale », mais après déploiement, il faudra observer la distribution de
compression_input_token_count/compression_output_token_countdans la télémétrie, ainsi que la qualité perçue après compression (retours utilisateurs, taux d’étatCOMPRESSION_FAILED_*). Si la baisse de qualité est significative, on pourra envisager de revenir à thinking activé avec un contrôle provider-specific du thinkingBudget. -
L’atteinte de
maxOutputTokenspeut tronquer le résumé. Avec thinking désactivé, 20K limitent directement le résumé ; claude-code mesure p99.99 ≈ 17K, laissant ~3K de marge de sécurité. Mais le prompt de compression de qwen-code diffère de celui de claude-code, la distribution doit être observée. Il est recommandé d’ajouter un chemin NOOP dans la branche d’échec de compression (chatCompressionService.ts:464-491) détectantfinish_reason = MAX_TOKENS, pour éviter de persister un résumé tronqué. -
Différences de mappage de
maxOutputTokensselon les fournisseurs. OpenAI compat (dashscope) →max_tokens, Anthropic →max_tokens, Gemini SDK →maxOutputTokens. qwen-code dispose déjà de ce mappage (contentGenerator.ts:94 etc.), il faut vérifier lors de l’implémentation de P6 que le champmaxOutputTokenssur le chemin sideQuery est bien propagé dans le corps de requête de tous les fournisseurs. -
L’estimation de tokens est une limite basse grossière, elle ne doit pas être utilisée en sens inverse pour sauter le déclenchement. L’écart
char/4avec les tokenizers réels des fournisseurs peut atteindre ±30 %. Cette spécification n’utilise l’estimation que pour « déclencher plus tôt » (direction faux-positif, mieux vaut compresser tôt que tard). Tous les chemins de code qui « réduisent le compteur de tokens / sautent la compression » doivent encore utiliserlastPromptTokenCount(valeur autoritaire de l’API). -
Relation entre la fonction d’estimation et
estimateContentCharsexistante. compactionInputSlimming.ts contient déjàestimateContentChars(utilisé pour le calcul des points de split de compression). La nouvelleestimateContentTokensdoit la réutiliser (diviser par bytesPerToken) plutôt que d’en écrire une nouvelle, pour éviter une divergence entre les deux mesures.
Hors périmètre de cette spécification
- Canal de surcharge via variable d’environnement (option D) : maintenir le principe de « configuration minimale ».
- Visualisation résidente dans le footer : réservé pour follow-up.
- Amélioration du prompt de résumé, ajustement de
MIN_COMPRESSION_FRACTION: orthogonaux à la conception des seuils.
Questions ouvertes (en attente de review)
- Intensité du breaking change : avertissement + champ ignoré vs erreur au démarrage. Option choisie : avertissement. À confirmer si suffisamment convivial pour les déploiements en entreprise/équipe.
Clôturé
- Sur les petites fenêtres (≤ ~76.7K), hard et auto se replient sur la même valeur — décision de ne pas l’indiquer clairement dans
/context. Raison :- Le repli ne concerne pas seulement 32K : toutes les fenêtres où
effectiveWindow - HARD_BUFFER ≤ 0.7 × windowsont repliées (y compris 64K). - Le comportement utilisateur reste le même : sur une fenêtre repliée,
currentTiersaute le niveau'auto'et indique directement'hard'(contextCommand.ts:43-44vérifie d’abord>= hard), la bandecontext-high(auto ≤ t < hard) devient vide. Une indication en moins sur les petites fenêtres est raisonnable — la fenêtre est de toute façon petite, l’utilisateur gère probablement le contexte manuellement. - Si à l’avenir des utilisateurs réels signalent « ne pas voir l’indication intermédiaire sur les petites fenêtres », on pourra décider d’ajouter un marquage UI ou d’ajuster la condition de déclenchement de
context-high(c’est du travail UI, non de spécification). Pour l’instant, on choisit de ne pas augmenter la complexité UI.
- Le repli ne concerne pas seulement 32K : toutes les fenêtres où