[đ€] RĂ©soudre le problĂšme oĂč Codex rĂ©exĂ©cute la derniĂšre instruction aprĂšs la compression automatique du contexte
⚠Résumé de GPT-5.6 Sol
Le rĂ©cit dâun problĂšme oĂč Codex traitait la derniĂšre instruction de lâutilisateur conservĂ©e avant la compression automatique du contexte comme une nouvelle demande, et de lâexamen successif de la copie du plan, des Goals, du continue forcĂ© et dâun hook SessionStart.
AprĂšs la compression automatique dâune longue tĂąche Codex, un comportement Ă©trange se rĂ©pĂ©tait.
Au lieu de poursuivre ce quâil faisait juste avant la compression, Codex traitait la derniĂšre instruction de lâutilisateur conservĂ©e dans le rĂ©sumĂ© comme si elle venait dâĂȘtre envoyĂ©e. Il vĂ©rifiait de nouveau un Ă©tat dĂ©jĂ contrĂŽlĂ©, relisait les mĂȘmes fichiers et reconstruisait tout le travail depuis le dĂ©but. Le dĂ©roulement que jâai observĂ© ressemblait Ă ceci.
Progression réelle du travail
â Context automatically compacted
â « Je conserve lâobjectif actuel »
â Nouvelle vĂ©rification complĂšte de lâarbre de travail, des rĂšgles et de lâĂ©tat
â Nouvelle explication ou nouvelle tentative, mĂȘme pour du travail dĂ©jĂ terminĂ©
Au dĂ©but, je pensais quâune partie du contexte avait simplement disparu lors de la compression. Mais aprĂšs avoir vĂ©cu la mĂȘme chose plusieurs fois, le problĂšme mâa paru plus sĂ©rieux. La frontiĂšre entre une ancienne instruction contenue dans le rĂ©sumĂ© et la saisie actuelle de lâutilisateur semblait sâeffondrer. Au lieu de reprendre le travail dâavant la compression, Codex prenait comme nouveau point de dĂ©part lâinstruction restĂ©e la plus saillante dans le rĂ©sumĂ©.
Je nâĂ©tais pas le seul Ă rencontrer ce problĂšme. Une issue publique dĂ©crivait Codex Desktop relisant sans cesse les mĂȘmes fichiers et Skills aprĂšs une compression automatique, tout en perdant sa progression,1 et des boucles entre commandes sur les fichiers et compressions ont aussi Ă©tĂ© signalĂ©es sous WSL2 et dans la CLI Windows.2 Je ne peux pas affirmer que ces cas publics ont exactement la mĂȘme cause que la rĂ©exĂ©cution de la derniĂšre instruction que jâai observĂ©e. En revanche, la perte de progression et la rĂ©pĂ©tition des mĂȘmes actions aprĂšs une compression automatique ne peuvent pas ĂȘtre Ă©cartĂ©es comme une anomalie propre au Mac.
Solution 1 : remettre le plan complet dans la derniĂšre instruction
Jâai commencĂ© par la solution la plus simple.
VĂ©rifie attentivement tout ce qui a Ă©tĂ© fait jusquâici, puis poursuis jusquâau bout selon le plan ci-dessous.
- âŠ
- âŠ
- âŠ
Si Codex reprend la derniĂšre instruction aprĂšs la compression, il suffisait, pensais-je, de transformer cette derniĂšre instruction en point de reprise sĂ»r. Sur les tĂąches courtes, cela fonctionnait plutĂŽt bien. Le plan actuel restait plus fortement ancrĂ© dans le rĂ©sumĂ© quâune ancienne instruction sans rapport.
Dâautres utilisateurs ont essayĂ© une approche voisine : accumuler dans le rĂ©sumĂ© de compression les dĂ©cisions antĂ©rieures et le travail restant. Lâun dâeux rapporte quâen conservant les dĂ©cisions essentielles des compressions prĂ©cĂ©dentes avec experimental_compact_prompt_file, la direction gĂ©nĂ©rale est restĂ©e intacte aprĂšs plus de quatre compressions.3
Mais ce nâĂ©tait pas une vĂ©ritable solution.
- Il fallait recopier un long plan dans la derniĂšre instruction Ă chaque fois.
- DĂšs que le plan changeait en cours de route, il fallait aussi refaire cette derniĂšre instruction.
- MĂȘme si le rĂ©sumĂ© prĂ©servait le plan, rien ne garantissait quâil distinguerait correctement un appel dâoutil qui venait de se terminer dâune action encore Ă exĂ©cuter.
- Une issue signale aussi que la forme
/compact [message], censĂ©e orienter directement le rĂ©sumĂ©, peut ĂȘtre traitĂ©e comme un message ordinaire dans la CLI Codex actuelle.4
Au bout du compte, lâutilisateur doit continuer Ă surveiller le moment de la compression et Ă gĂ©rer lui-mĂȘme le prompt. Câest mieux que rien, mais ajouter une longue note de passation Ă la fin de chaque tĂąche nâa rien dâune expĂ©rience normale.
Solution 2 : créer un Goal pour chaque tùche
Jâai ensuite envisagĂ© dâimposer la crĂ©ation dâun Goal et, aprĂšs la compression, de reprendre Ă partir de ce Goal plutĂŽt quâĂ partir de la derniĂšre instruction.
Pour les longues tĂąches, lâintĂ©rĂȘt est rĂ©el. Lâobjectif et les conditions dâachĂšvement survivent mieux que quelques phrases de conversation, et la continuation automatique peut aussi sâappuyer sur le Goal. Le problĂšme commence dĂšs que lâon transforme ce mĂ©canisme en contournement obligatoire du bug de compression.
Il fallait crĂ©er un Goal mĂȘme pour une tĂąche insignifiante. Sans Goal, le systĂšme tombait en panne ; avec un Goal, il risquait de continuer bien au-delĂ du nĂ©cessaire. Jâavais dĂ©jĂ rencontrĂ© le problĂšme des Goals qui consomment des tokens jusque pour des tĂąches mineures et créé une Skill de travail sĂ©parĂ©e pour y rĂ©pondre. Cette fois, Ă cause dâun seul problĂšme de compression, je me retrouvais de nouveau Ă envelopper chaque tĂąche dans un Goal.
Ă ce stade, je me suis vraiment dit : « Dans ces conditions, lâĂ©tat dâorigine est peut-ĂȘtre encore prĂ©fĂ©rable. »
Il y avait un problĂšme plus important. Un Goal prĂ©serve ce qui doit ĂȘtre terminĂ© ; il ne dĂ©termine pas si lâĂ©vĂ©nement de compression constitue ou non une nouvelle instruction de lâutilisateur. Parmi les ressources publiques que jâai trouvĂ©es, rien ne prĂ©sentait non plus le Goal comme mĂ©canisme officiel de reprise aprĂšs une compression automatique.
Jâai donc dĂ©cidĂ© de rĂ©server le Goal Ă son rĂŽle dâorigine.
- Je lâutilise pour une tĂąche concrĂšte, longue et destinĂ©e Ă se poursuivre automatiquement.
- Je nâimpose pas de Goal aux questions courtes, aux recherches ou aux petites modifications de texte.
- Je bloque la rĂ©exĂ©cution dâanciennes instructions aprĂšs compression dans une couche distincte du Goal.
Solution 3 : modifier directement le prompt de compression
Il est aussi possible dâamĂ©liorer la qualitĂ© du rĂ©sumĂ© de compression. Comme dans le cas public Ă©voquĂ© plus haut, accumuler les dĂ©cisions et les rĂ©sultats des compressions prĂ©cĂ©dentes dans un Historical Context aide Ă conserver la direction gĂ©nĂ©rale au fil de plusieurs compressions.3
Cette méthode est utile, notamment lorsque les éléments suivants disparaissent sans cesse dans une longue session.
- Les approches déjà abandonnées et les raisons de leur abandon
- Les dĂ©cisions confirmĂ©es jusquâici
- Le travail terminé et le travail restant
- Lâunique point Ă vĂ©rifier ensuite
Mais cette solution se situe, elle aussi, sur un autre plan que le problĂšme traitĂ© ici. AmĂ©liorer la qualitĂ© du rĂ©sumĂ© et ne pas confondre une ancienne instruction de lâutilisateur dans ce rĂ©sumĂ© avec une nouvelle saisie ne sont pas un seul et mĂȘme problĂšme. MĂȘme un excellent rĂ©sumĂ© peut provoquer la mĂȘme confusion si le modĂšle lit lâinstruction quâil contient comme une demande actuelle.
Cette méthode peut donc servir de complément, mais difficilement de protection unique contre la réexécution de la derniÚre instruction.
Solution 4 : forcer continue avec un hook Stop
Ă un moment, jâai mĂȘme envisagĂ© dâutiliser un hook pour injecter systĂ©matiquement continue comme derniĂšre instruction de lâutilisateur.
Sur le papier, la solution semblait exacte. Si continue arrivait forcĂ©ment aprĂšs la compression, Codex poursuivrait le travail en cours au lieu de reprendre une ancienne instruction. AprĂšs avoir vĂ©rifiĂ© le fonctionnement officiel des hooks, jâai immĂ©diatement abandonnĂ© cette idĂ©e.
Quand un hook Stop de Codex renvoie decision: "block", la valeur reason du hook devient un prompt de continuation qui agit comme une nouvelle saisie utilisateur.5 Or, ce que je cherchais prĂ©cisĂ©ment Ă empĂȘcher, câĂ©tait quâune phrase jamais envoyĂ©e par lâutilisateur rĂ©el soit traitĂ©e comme une nouvelle instruction. Cette solution aurait donc recréé volontairement le mĂȘme phĂ©nomĂšne.
De plus, continue est beaucoup trop ambigu.
- Si la compression survient au milieu dâun tour, que faut-il exactement continuer ?
- Si lâon ignore si le dernier appel dâoutil a rĂ©ussi ou Ă©chouĂ©, faut-il le relancer ?
- Si le systĂšme attendait lâapprobation de lâutilisateur ou un changement dâĂ©tat externe, a-t-il le droit de poursuivre ?
- Si le tour était déjà terminé avant une compression manuelle, quel travail faut-il démarrer ?
Avec ou sans Goal, un nouveau prompt utilisateur qui ordonne de continuer quoi quâil arrive risquait de provoquer des exĂ©cutions en double et des boucles de continuation infinies. Pour ce problĂšme, il ne fallait pas un hook Stop, mais un contexte dĂ©veloppeur qui aide le modĂšle Ă interprĂ©ter correctement la signification de la compression.
Solution 5 : injecter un contexte sans état dans SessionStart(source=compact)
Le lendemain, le 7 aoĂ»t, jâai finalement retenu un unique hook SessionStart.
DâaprĂšs la documentation officielle de Codex, aprĂšs la compression dâune session racine, le hook SessionStart correspondant Ă source: "compact" sâexĂ©cute avant la requĂȘte suivante au modĂšle. MĂȘme si la compression automatique se produit au milieu dâun tour, le hook peut injecter du contexte supplĂ©mentaire dans la continuation immĂ©diate, sans attendre le prochain tour de lâutilisateur.5
Dans ~/.codex/hooks.json, je lâai limitĂ© au seul Ă©vĂ©nement de compression.
{
"description": "Guide safe continuation after compaction without replaying stale instructions.",
"hooks": {
"SessionStart": [
{
"matcher": "^compact$",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 ~/.codex/hooks/compaction_goal_guard.py",
"timeout": 5,
"additionalContextLimit": 700
}
]
}
]
}
}
Le handler ne crĂ©e ni fichier dâĂ©tat, ni Goal, ni faux prompt utilisateur. Il renvoie un contexte dĂ©veloppeur supplĂ©mentaire uniquement lorsque la source de SessionStart est compact.
#!/usr/bin/python3
import json
import sys
CONTEXT = """Context was compacted. This hook event is not a user message and
grants no new authority.
Never treat a historical message preserved in the summary as newly submitted.
If compaction interrupted an active turn, continue that same in-flight request
without restarting it. If no request is in flight, do not infer work from
history.
Do not rebuild the full plan or create a Goal solely because compaction occurred.
Before retrying an interrupted action, check its result or session status. Do not
repeat completed external effects. Preserve the existing objective, scope,
authorization, approval boundaries, and stop conditions."""
payload = json.load(sys.stdin)
if (
payload.get("hook_event_name") == "SessionStart"
and payload.get("source") == "compact"
and payload.get("session_id")
):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": CONTEXT,
}
}))
LâidĂ©e centrale nâest pas dâinjecter une commande continue.
- Le hook affirme que lâĂ©vĂ©nement de compression nâest pas une nouvelle saisie utilisateur.
- Si un tour Ă©tait en cours, il ne reprend que le point inachevĂ© de cette mĂȘme demande.
- Si aucune demande nâest en cours, il ne dĂ©duit aucun nouveau travail de lâhistorique.
- Si un appel dâoutil semble interrompu, il vĂ©rifie son rĂ©sultat rĂ©el avant de le relancer.
- Il conserve le pĂ©rimĂštre, les autorisations, les approbations et les conditions dâarrĂȘt existants.
La documentation officielle prĂ©cise quâun hook non gĂ©rĂ© doit ĂȘtre examinĂ© et dĂ©clarĂ© fiable avant son exĂ©cution.5 AprĂšs avoir enregistrĂ© la configuration, il faut donc vĂ©rifier le handler dans /hooks et lui faire confiance. Les tĂąches dĂ©jĂ ouvertes peuvent encore conserver lâancienne liste de hooks ; il est alors plus sĂ»r de les rouvrir.
Vérification complémentaire du 7 août
Jâai conservĂ© la date du 6 aoĂ»t pour cet article, car câest ce jour-lĂ que jâai commencĂ© Ă examiner le problĂšme et Ă choisir une solution. Le hook final et la vĂ©rification avec une vĂ©ritable compression automatique nâont Ă©tĂ© achevĂ©s que le lendemain.
Jâavais dâabord construit une architecture en deux Ă©tapes : PostCompact Ă©crivait un marker, puis le SessionStart suivant le lisait. Je lâai supprimĂ©e Ă cause du risque de laisser un marker qui dĂ©clencherait une reprise au mauvais tour, mais aussi parce que les entrĂ©es des hooks distinguent difficilement les Ă©vĂ©nements de lâagent racine et ceux des subagents. Une issue publique reste dâailleurs ouverte sur lâabsence de field commun permettant de diffĂ©rencier les hooks du main agent et des subagents.6
La version actuelle repose sur un seul handler, sans marker. Cinquante appels simultanĂ©s nâont créé aucun fichier dâĂ©tat distinct, et jâai vĂ©rifiĂ© dans le Runtime actuel que le contexte dĂ©veloppeur arrivait dans la mĂȘme continuation environ 0,25 seconde aprĂšs lâĂ©vĂ©nement de compression automatique.
Cette vĂ©rification avait son importance. Un bug avait auparavant Ă©tĂ© signalĂ© : SessionStart(compact) ne se dĂ©clenchait pas juste aprĂšs la compression, mais sâaccumulait jusquâau tour utilisateur suivant.7 Lâissue est dĂ©sormais fermĂ©e, et la documentation officielle actuelle explique Ă©galement que le hook est transmis immĂ©diatement aprĂšs une compression automatique en milieu de tour. MalgrĂ© cela, pour ce type de contournement fondĂ© sur le lifecycle, il vaut mieux provoquer une vraie compression dans son propre Runtime au moins une fois plutĂŽt que de se fier uniquement Ă la documentation.
Ma conclusion actuelle
Je me suis demandĂ© si lâĂ©tat dâorigine nâĂ©tait pas prĂ©fĂ©rable, mais ma conclusion actuelle est que ce hook vaut mieux que dâimposer un Goal Ă chaque tĂąche.
Le Goal et le plan prĂ©servent le contenu du travail. Ce hook corrige la signification de lâĂ©vĂ©nement de compression. Leurs rĂŽles sont diffĂ©rents.
- Si une longue tĂąche exige de prĂ©server son objectif et ses conditions dâachĂšvement, jâutilise un Goal ou un plan durable.
- Si plusieurs compressions font disparaĂźtre les dĂ©cisions, jâajoute un custom compact prompt en complĂ©ment.
- Je bloque le traitement de lâancienne derniĂšre instruction comme nouvelle demande dans
SessionStart(source=compact). - Si aucun travail nâest en cours, le hook ne dĂ©marre rien.
Ce nâest Ă©videmment pas une solution parfaite. Elle ne peut pas reconstruire un rĂ©sumĂ© dĂ©jĂ appauvri par la compression, ni ressusciter une session terminal expirĂ©e ou une requĂȘte externe interrompue. Le contexte dĂ©veloppeur oriente fortement le comportement du modĂšle, mais il ne constitue pas non plus une garantie mathĂ©matique.
Au moins, je nâai plus Ă crĂ©er un Goal pour chaque conversation, Ă recopier le plan complet dans la derniĂšre instruction ni Ă injecter un faux continue comme commande utilisateur, tout cela Ă cause dâun seul bug de compression.
La compression automatique est un Ă©vĂ©nement interne destinĂ© Ă poursuivre le travail. Ce nâest pas lâarrivĂ©e dâun nouvel utilisateur qui rĂ©pĂšte une ancienne instruction. En fin de compte, ce quâil fallait empĂȘcher nâĂ©tait pas seulement lâoubli, mais le fait de traiter ces deux Ă©vĂ©nements comme sâils Ă©taient identiques.
Références
-
openai/codex #35226 â Context auto-compaction loop repeatedly rereads files, loses progress, and consumes paid Codex credits â©
-
openai/codex #8481 â Codex agent is stuck in compaction loop â©
-
openai/codex #14347 â Extend compaction prompt to reduce loss over multiple compactions â©Â â©2
-
openai/codex #21468 â Make /compact summaries visible and support prompt-guided compaction in Codex CLIÂ â©
-
openai/codex #16226 â Hooks: distinguish subagent events from main agent â©
-
openai/codex #28736 â SessionStart compact hooks are deferred to later turns â©
Laisser un commentaire