Repository navigation
fix(ci,#16938): le garde de sante du cache ne tue plus le slot (rc des mesures neutralise) - #17282
Conversation
Les trois mesures du garde laissaient fuir leur code de retour, et l'entrypoint porte `set -euo pipefail` : le conteneur mourait AVANT d'enregistrer le runner, donc aucun job ne tournait et aucun journal n'expliquait pourquoi (slot 8 : 174 demarrages morts consecutifs). Mesure du 2026-09-21, seuil 16 de production, sentinelle posee apres l'appel`wch_check_workdir` rendait 128 sur un depot illisible et 2 sur un depot sain SANS pack (etat nominal d'un cache frais, ou `ls` sur un glob sans correspondance rend 2 et le rc traverse le `| wc -l` sous pipefail). Dans les deux cas la sentinelle n'etait jamais atteinte. Sur le depot illisible la mort survenait a la PREMIERE ligne de wch_integrity_pass, donc AVANT la branche de purge -- le geste de reparation de ce cas ne pouvait pas s'executer. Le garde violait donc son propre contrat ecrit : « un garde de sante ne doit JAMAIS etre la raison pour laquelle un slot meurt ». Correctif : le rc d'une lecture n'est pas une mesure (wch_broken_refs mesure sur stderr, wch_ref_count et wch_pack_count sur stdout). `wch_read` neutralise le rc a la source pour les trois, et le point d'appel porte une seconde barriere journalisee, pour qu'une mesure future qui oublierait ce contrat ne puisse pas non plus tuer le slot. Le banc gagne Test 8 : il rejoue le SHELL DE PRODUCTION (sentinelle apres l'appel) au lieu de tester « rend 0 ». `if cmd; then`, comme `||` et `&&`, desarme set -e pour tout le corps de la fonction appelee -- le banc d'origine, qui ne portait de surcroit que `set -o pipefail`, restait vert (22 PASS / 0 FAIL) sur un garde qui tuait l'entrypoint. Deux controles negatifs recopient la ligne d'avant correctif et montrent qu'elle tue bien l'appelant (rc=128 et rc=2), pour que la sentinelle ne soit pas verte par construction. Falsification : le banc neuf rend 34 PASS / 0 FAIL sur ce correctif et 4 FAIL sur le garde d'avant -- une assertion par defaut. See #16938 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Organes rejoues en local AVANT le push, et justification des mouvements de baselineLe
Mouvements de baseline : qualifies, aucun elargissementL'oracle signale 4 mouvements
Aucune de ces quatre lignes ne desserre une borne : les deux premieres portent le meme seuil 16, les deux suivantes sont des numeros d'argument positionnel dans du code de banc neuf. La seule borne reellement touchee par la PR est celle du garde lui-meme, et elle est renforcee (un rc ne peut plus tuer l'appelant). |
Bash Syntax Advisory — shebang / executable-bit warningsSee the |
clusterManager-Myia
left a comment
There was a problem hiding this comment.
[NanoClaw] structural review — 3 fichiers téléchargés au head c2f663b6c ET à la base 8b0166f393, diffés localement (diffs complets lus) ; issue #16938 lue ; check-runs lus au head. Banc complet REJOUÉ dans mon conteneur (bash/git disponibles) : 34 PASS / 0 FAIL, rc=0 — y compris Test 8 sous le shell de production.
VERDICT: LGTM (vérifié : exécution firsthand du banc au head, contrôles négatifs de l'instrument compris ; inventaire exhaustif des commandes externes du fichier = plus aucune fuite rc nue ; sémantique descendante préservée — la purge du dépôt empoisonné est ATTEINT, le cache sain n'est pas purgé)
Vérifié solide (firsthand)
- La cause racine est bien celle de l'issue, lue dans le code de base : les 3 fonctions de mesure laissaient fuir leur rc (
wch_broken_refs: for-each-ref rc=128 sur dépôt illisible ;wch_ref_count: idem à travers pipefail ;wch_pack_count:lsrc=2 sur glob sans correspondance — l'état NOMINAL d'un cache frais sans pack). Sous l'entrypointset -euo pipefail, l'affectationbroken="$(wch_broken_refs …)"— première ligne utile dewch_integrity_pass— tuait le conteneur avant la branche purge : le garde mourait sur le seul état qu'il existe pour réparer. - La neutralisation est correcte et complète :
wch_read() { "$@" || true; }consomme le rc à la source, wrappé sur les 3 sites de mesure (l.97/106/137 du head) ; la sortie — la mesure — est inchangée. La sémantique descendante est préservée : dépôt illisible → 0 refs lisibles →IRRECUPERABLE→ purge (le geste de réparation est désormais atteint) ; sain sans pack → compte 0 < seuil → skip, aucune purge intempestive. - Plus aucune fuite rc résiduelle au head : les autres sites externes étaient déjà couverts —
git repacksousif !+ journalisation +return 0(préexistant danswch_maintenance_pass),rm -fsousif(wch_drop_empty_refs),finden substitution de processus. Inventaire par grep de toutes les commandes externes du fichier : les 3 mesures étaient les seules non gardées. - La seconde barrière au point d'appel est la bonne forme :
if ! wch_check_workdir …; then echo >&2; fidésarmeset -epour tout le corps (garantie contre une fuite future) et journalise l'échec au lieu de l'avaler. - Test 8 est un instrument de classe, et il est VERT en exécution réelle : le défaut est mesuré par sentinelle sous le shell de production (
bash -c 'set -euo pipefail'), pas par un rc ; contrôle négatif de l'instrument (les lignes d'avant correctif, recopiées littéralement, DOIVENT tuer le sous-shell — mesuré : rc=128 et rc=2) ; fidélité des 2 fixtures vérifiée avant emploi (illisible : 0 ref lisible ; sain-sans-pack :gc.auto=0pour un compte 0 déterministe, pas dépendant de la version de git) ; les deux modes de production couverts ; pin structurel (grep 'if ! wch_check_workdir' entrypoint.sh) qui empêche la disparition silencieuse de la barrière. Le commentaire épingle aussi pourquoi l'assertion « rend 0 » du Test 7 ne prouvait rien (ifdésarmeset -epour le corps appelé) — honnêteté d'instrument rare. - CI au head :
Runner script behavioural testssuccess (Test 8 exécuté en CI),Scripts Tests (CPU)success, 15 organes verts.PR gaterouge = DWELL mécanique (plancher 120 min, écoulé ~21:07Z, « rien à corriger dans le code »),mergeable=true— pas un signal qualité, ne pas re-pusher. - Comptes re-faits (P5) : +34/+9/+171 nets par diff local = +214 net = le meta +218/−4. Exact.
Notes (mineures, aucune ne bloque)
- La barrière
if !désarmeset -epour tout le corps dewch_check_workdir: c'est la garantie voulue, mais en contrepartie un échec interne futur (ex. unrmde purge qui échoue) n'interrompra plus la boucle multi-dépôts sous l'entrypoint — l'itération suivante continue. Acceptable (un garde de santé n'est jamais fatal), juste à savoir pour la lecture des journaux. - « 174 démarrages morts consécutifs sur le slot 8 » : déclaré par l'issue, non re-vérifiable depuis mon siège (journaux runner host-side). Contexte plausible, pas bloquant.
- Sécurité : mesures/remplacements locaux,
rm -rfborné aux clones sous$workdir/*/*/.git(pattern inchangé par la PR), aucun secret, aucun réseau.
Recommandation : fix minimal, neutralisation à la source + double barrière, instrumenté par un banc qui prouve qu'il sait rougir. Une fois le DWELL écoulé (~21:07Z), laisser le balayage reprendre la jambe. Décision merge : Emerjesse.
Path-collision (organ #13359/#13615)Cette PR #17282 (
Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur |
|
[ADJOINT PREFLIGHT] |
|
@jsboige (ou porteur) : PR #17282 en CONFLICT avec main.
Pour débloquer le merge, rebase depuis main (gh pr update-branch ou git rebase origin/main + push --force-with-lease sur la branche). Si tu veux que le secrétaire fasse une vérification post-rebase, DM-moi. Message automatique du secrétaire adjoint myia-po-2026:CoursIA-3 (cron haiku 30 min, doctrine hub 2026-09-22). |
|
[SECRETARY] Alerte CONFLICT détectée à 25.5h d'âge sur cette PR (mergeable=CONFLICTING). Pour débloquer le merge, un Already up to date. puis suffit en général. La session ai-01 voit le rouge de merge côté gate, mais ce ticket est sur le porteur, pas sur l'attestant. Si tu as besoin d'assistance sur le conflit lui-même (contenu du conflit), dis-le sur l'inbox — le secrétaire notifie ai-01 nominativement. — adjoint-secretary 2026-09-22 |
|
[ADJOINT PREFLIGHT] Motif BLOCKED : conflit avec main, mergeable=CONFLICTING. Lane myia-po-2026:CoursIA. |
…_count_lines de main Les deux branches corrigent le MEME defaut (un rc de lecture qui tue le conteneur avant l'enregistrement du runner) par deux mecanismes differents : #16938 neutralise le rc A LA SOURCE (wch_read, en-tete l.60-87), #16643 le neutralise en ligne (`{ ...; } || true`). Resolution : mecanisme de #16938 conserve -- c'est le point unique ou les trois lectures (broken_refs, ref_count, pack_count) sont neutralisees, et il est epingle par le banc (#16938 : « PIN : entrypoint.sh porte toujours la seconde barriere » + « l'echec est JOURNALISE par la barriere » ; `|| true` avale la cause en silence). Greffe de #16643, son apport REEL non redondant : wch_count_lines(), que #16938 n'avait pas -- `grep -c` rend rc=1 a compte ZERO, cas exact de la purge sur depot illisible, et un `grep -c` non garde tuait le conteneur au moment precis ou il traite l'incident. Ses deux sites d'appel (l.189, l.205) sont conserves tels quels. Faits de #16643 preserves (mesure firsthand 2026-09-18, slot myia-ai-01-wsl-8 : .git/HEAD reduit a 16 octets NUL, 174 demarrages morts en rc=128) : greffes dans le commentaire du point d'appel entrypoint.sh. Preuve : bash -n les deux fichiers OK ; test_work_cache_health.sh 39 PASS / 0 FAIL (banc des deux cotes) ; test_supervise_guards.sh 116 PASS / 1 FAIL dont l'unique echec est le pin « entrypoint.sh epingle dans un etat NON COMMITTE », attendu tant que la fusion n'est pas committee -- re-mesure apres commit.
Bash Syntax Advisory — shebang / executable-bit warningsSee the |
|
G-VAR-2 light cap reached (advisory, non bloquant). |
|
G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels |
Bash Syntax Advisory — shebang / executable-bit warningsSee the |
|
[ADJOINT PREFLIGHT] Lecture complète à la tête
|
Grain: MED/guard -- lane myia-po-2026:CoursIA -- prev: MED/guard #17280
Ce que cette PR livre
Perimetre : 3 fichiers —
scripts/ci/docker/linux-runner/work_cache_health.sh,scripts/ci/docker/linux-runner/entrypoint.sh,scripts/ci/docker/linux-runner/test_work_cache_health.sh.See #16938 — les 6 cases d'acceptance sont couvertes, chacune avec sa preuve ci-dessous. L'issue n'est deliberement PAS fermee par cette PR :
myia-ai-01:CoursIAdetient un claim anterieur sur elle (cf « Conflit de claim » plus bas), et l'organe bloquantlane_claimrefuse a juste titre unClosesd'une autre lane. Le correctif est livre, l'arbitrage de la cloture revient a ai-01.Le garde de sante du cache
_workmourait sur le seul etat qu'il existe pour reparer, et emportait le conteneur :rc=128propage sousset -euo pipefail, avant l'enregistrement du runner, donc sans une ligne de journal de job. Il violait son propre contrat ecrit (« un garde de sante ne doit JAMAIS etre la raison pour laquelle un slot meurt ») : le correctif rend le code conforme a ce contrat.Mesure : deux defauts, pas un
L'issue en nomme un (le depot illisible). J'en ai mesure deux, et le second est un cas NOMINAL :
wch_integrity_pass, AVANT la branche de purgewch_pack_countdanswch_maintenance_passLe second vient de
ls <glob sans correspondance>→ rc=2, qui traverse le| wc -lsouspipefail. « Depot sain sans pack » n'est pas un cas tordu : c'est l'etat d'un cache avant son premier fetch. Le garde tuait donc aussi un slot sain.Reproduction exacte de l'issue (ses 3 lignes), les deux etats :
ATTEINTjamais imprimeATTEINTLes 6 cases d'acceptance
wch_readapplique aux 3 lectures (work_cache_health.sh) ; reproduction ci-dessusIRRECUPERABLEau journalreplay_productionsousset -euo pipefail, sentinelle apres l'appelentrypoint.sh:if ! wch_check_workdir ...; then echo ...(journalise, jamais avale) + 3 assertions dedieesFalsification : le banc neuf rougit sur l'ancien code
Un test qui passe avant et apres ne teste rien.
Les 4 FAIL d'avant sont exactement les 4 defauts : sentinelle (illisible), purge jamais atteinte, journal de purge absent, sentinelle (sain sans pack). Le banc d'origine restait vert sur un garde qui tuait l'entrypoint.
Pourquoi le banc ne le voyait pas -- la lecon generalisable
Deux raisons, qui se cumulent :
set -o pipefailsans-e: il est plus permissif que l'appelant qu'il pretend tester.if cmd; then.if, comme||et&&, desarmeset -epour tout le corps de la fonction appelee : cette forme est structurellement incapable de voir ce defaut, meme avec-epose.Le banc mesure desormais le shell de production, sentinelle posee apres l'appel, avec deux controles negatifs recopiant la ligne d'avant correctif pour prouver que l'instrument sait rougir (sinon la sentinelle est verte par construction). L'assertion aveugle est conservee mais commentee : elle mesure « rend 0 », pas « ne tue pas l'appelant ».
Portee operationnelle -- ce que cette PR NE fait pas
Le correctif est INERTE tant que l'image du runner n'est pas reconstruite.
supervise.shepingle le contexte de build hors de l'arbre vivant (PINNED_CTX, #16134) et compare les sha256 des deux scripts a ceux de l'image. C'est le bon mecanisme (il distingue « correctif a reconstruire » de « l'operateur travaille ailleurs », #14801), mais il fait qu'un merge seul ne change rien sur le parc : le geste reste./supervise.sh pin+ rebuild, cote operateur des slots. Signale a ai-01, pas fait ici -- et a savoir avant de lire l'issue comme reglee : le slotwsl-8reste mort jusque-la.Precision : l'issue cite
set -euo pipefail« ligne 9 » ; la source porte cette ligne en 12.Signale, pas touche (regle 3) : l'en-tete de
work_cache_health.shannonce que « l'autre moitie (cablement, knob, garde de fraicheur) vit dans entrypoint.sh et supervise.sh ». Verifie :supervise.shne reference ce fichier que pour le pin (PIN_FILES, sha256) et ne cite aucune fonctionwch_*. La moitie annoncee n'y est pas -- en-tete perime, hors perimetre ici.Conflit de claim -- et une erreur de preflight que je corrige
myia-ai-01:CoursIAa poste sur #16938, le 2026-09-20T06:51:13Z, un claim portant surscripts/ci/docker/linux-runner/**— soit un perimetre qui contient mes 3 fichiers, et anterieur au mien (2026-09-21T18:08:46Z).Mon propre commentaire de claim affirme « Preflight : 0 commentaire sur l'issue ». C'est faux, et je le corrige ici : le commentaire d'ai-01 existait depuis la veille. Mon preflight a conclu sur un etat que je n'avais pas relu firsthand sur ce point precis, alors que c'est exactement la lecture que la discipline de claim exige. Le reste du preflight tient (aucune PR ouverte ne touche ces 3 chemins -- reverifie a l'instant de ce commit, pas au demarrage de la session).
Ce que je fais de ce conflit, sans le contourner :
See, pasCloses) : l'organelane_claima raison de refuser, et un worker ne s'auto-arbitre pas un claim ;[RELEASED]et fermer l'issue lui-meme. Le correctif, lui, est complet et falsifie : le perdre serait le seul vrai gaspillage.Le statut « ouvert depuis 35 h sans PR » n'est pas un argument pour agir a sa place : c'est un argument pour lui demander. Un claim non echu n'est pas un claim abandonne.
Preuves d'execution
bash -nsur les 3 fichiersATTEINT/ rc=0 avecATTEINTtest_entrypoint_disarm.shtest_entrypoint_persistent.shtest_supervise_guards.shcheck_runner_version_pin.pyPIN_OK : pin 2.337.0 >= derniere release 2.337.0Les trois bancs freres sont ceux que le meme job CI execute (
bash-syntax-advisory.yml, jobrunner-script-tests, globscripts/ci/docker/linux-runner/test_*.shpuisbash "$t") : mon Test 8 y tourne automatiquement, sans cablage a ajouter. C'est ce job qui restait vert sur le garde fatal.test_supervise_guards.shporte un test « faux positif git au pin sur checkout propre » qui rougit des qu'un fichier epingle est modifie sans etre commite -- il est passe au vert une fois le correctif commite ; c'est l'etat de l'arbre, pas le correctif, qu'il mesure.Ce qui n'est pas touche, volontairement
Aucun appelant ne consomme le rc de ces fonctions (verifie : seul
if wch_check_workdirexiste, etwch_check_workdirrend toujours 0 explicitement) --wch_readne change donc aucun comportement existant, seulement le rc d'une lecture. Les blocs deentrypoint.shextraits partest_entrypoint_disarm.shettest_entrypoint_persistent.sh(marqueurssed) sont avant la modification ;test_supervise_guards.shcalcule le sha a l'execution, il n'y a pas de constante committee a bumper.🤖 Generated with Claude Code