Skip to content

Fix(picker,#22630): cache local des etats de PR et du pool -- cibles de latence NON atteintes, residuel mesure - #22684

Merged
myia-ai-01 merged 8 commits into
mainfrom
fix/gh-snapshot-cache-22630
Oct 27, 2026
Merged

myia-ai-01 merged 8 commits into
mainfrom
fix/gh-snapshot-cache-22630

Conversation

@jsboige

@jsboige jsboige commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/tooling — lane myia-ai-01:CoursIA-2 — prev: MED/tooling #22639

Cibles NON atteintes.

Mesure Durée Appels gh
Chaude, dos à dos (instantané chaud) 127,4 s 80
Chaude après ~2 min (pool et énumération persistés) 157,7 s 102
Chaude après ~12 min (TTL expirés) 373,0 s 236
Froide (LOCALAPPDATA isolé) 1033,0 s 266

Cibles de l'issue : < 10 s et <= 5 appels gh en chaud, < 60 s en froid. La meilleure passe chaude mesurée (80 appels) reste 16 fois au-dessus du plafond d'appels, et le froid est 17 fois au-dessus de sa cible. Le dispositif de cache fonctionne ; il ne suffit pas, et les causes sont mesurées ci-dessous.

Ce qui est livré

  1. Cache de transport de l'organe B.0, plafonné. _B0TransportCache (scripts/pick_idle_grain.py) mémorise les payloads de check_unaddressed_nits.gh_json par (PR, updatedAt), sans modifier le fichier de l'organe. Invalidation identique à celle des états de PR : une PR qui ne bouge pas ne se relit pas. Une levée, une panne et un budget_exhausted ne sont jamais persistés — la passe suivante réessaie (3 tests dédiés).
  2. pool et claims_open_prs persistés immédiatement. Preuve du besoin : le flush de red_backlog s'exécute avant fetch_pool, et les sorties --json / --belt rendent la main sans repasser par la fin de main. Sans le flush immédiat ajouté dans fetch_pool et _complete_open_prs, ces deux entrées n'atteignaient jamais le disque (vérifié : absentes de repair.json sur deux passes consécutives, présentes après le correctif).
  3. Détection de livraison : NON faite, et je le dis plutôt que de la maquiller. pr list --state merged --search + pulls/N/files coûtent ~10 appels par passe ; les brancher demandait de recomposer la logique de couverture par PR, ce qui n'était pas « gratuit » au sens de la consigne.

Au passage, deux corrections de robustesse trouvées en chemin : snapshot_lock rendait la main sans détenir le verrou après expiration (deux lanes pouvaient écrire en même temps) — il lève désormais SnapshotLockTimeout et n'entre jamais dans le bloc protégé ; et une panne du verdict annexe B.0 n'est plus persistée (la passe suivante réessaie, comme sur la voie historique).

Cause exacte du résiduel, mesurée

(a) Les payloads B.0 coûteux dépassent le plafond de mémoïsation. commits/<sha> (38 appels), pr view (11), timeline (10), reviews (10) pèsent plus de 64 ko chacun. Sans plafond, 14 PRs écrivaient 29,6 Mo (6 Mo pour une seule) et la passe régressait à 360 s / 242 appels : le coût de lecture du fichier dépassait le gain. Les budgets retenus sont donc B0_PAYLOAD_MAX_BYTES = 64 ko par payload, 1 Mo par PR, 4 Mo pour le namespace — et ces appels sont relus à chaque passe où la PR bouge.

(b) L'arithmétique des TTL interdit tout bénéfice en exploitation normale. Une passe dure 127 à 373 s ; le pool est mémorisé 90 s et les commentaires 600 s. L'âge d'une entrée au moment de sa relecture vaut « durée de la passe + écart entre passes » : aucun TTL ne peut servir deux passes successives, et un cycle cron de 30 min les dépasse tous. Mesuré : deux passes dos à dos partagent le cache (80 appels) ; une passe ordinaire, 12 minutes plus tard, le paie intégralement (236 appels).

Il faut donc choisir explicitement :

  • rejeu immédiat — TTL supérieur à la durée d'une passe (~300-400 s) : utile à la relance après incident, sans effet sur le cron ;
  • cycle cron — TTL ≥ 1800 s : le cache sert enfin la cadence réelle, au prix de verdicts plus vieux (un DWELL ou un CHANGES_REQUESTED peut dater de 20-30 min).

Ce choix est un arbitrage de sémantique, pas un réglage : il appartient au coordinateur.

Coût assumé

repair.json passe de 2,6 Mo à 10,84 Mo : pool 3,18 · b0 2,81 · claims_open_prs 1,52 · open_prs 1,42 · states 0,97 · reste 0,14. Sans plafond il atteignait 37,7 Mo. Le plafond b0 (4 Mo) n'est pas atteint. Ce fichier est un cache de machine sous %LOCALAPPDATA%, jamais versionné.

Voie restante, nommée

Réduire ce que l'organe check_unaddressed_nits transporte (par exemple ne plus lire les files de chaque commits/<sha>, ou projeter les payloads avant mémoïsation) — hors du périmètre de fichiers de cette PR, qui s'arrête à pick_idle_grain.py, gh_snapshot.py et leurs tests. Alternativement, trancher les TTL comme ci-dessus. Cette voie restante est ouverte en issue de suivi : #22757.

Recouvrement avec #22636

Les deux PR modifient scripts/pick_idle_grain.py (cette PR : +658/−59 ; #22636 : +357/−47). Les zones modifiées ne sont pas disjointes : la fusion de mesure (git merge-tree --write-tree entre 10e9ce8302 et a41b5f104b) produit un CONFLICT (content) dans scripts/pick_idle_grain.py. Les symboles touchés diffèrent (_has_delivered_marker, fetch_pool, _snapshot_comments, _belt_snapshot_prime d'un côté ; container_marker_probe, has_recent_container_delivery_marker, belt_pick_with_replacements de l'autre), mais le fichier est le même et la fusion exige une résolution à la main, hunk par hunk, en comprenant les deux côtés — pas un « accepter les deux » réflexe.

Ordre de merge attendu : #22636 d'abord (elle est APPROVED et ne conflictait plus avec main), puis rebase de celle-ci. La base de cette branche est 131958b1, ancêtre de main (0df66d391b au moment de la rédaction) : le rebase est donc requis, et il ré-armera le plancher DWELL de cette PR.

(Résolu depuis, autrement : main a été fusionné dans la branche et le conflit résolu à la main — ccb3e24ff5c puis ba5fe9ec35a — cf. le suivi post-review ci-dessous. Le paragraphe ci-dessus est conservé comme mesure datée.)

Validation

  • python -m pytest scripts/tests/test_gh_snapshot_22630.py scripts/tests/test_pick_idle_grain.py scripts/tests/test_pick_idle_grain_wip.py scripts/tests/test_pick_board.py scripts/tests/test_pick_filters.py -q → 380 passed (rejoué après le commit 10e9ce8302).
  • scripts/tests/test_gh_snapshot_22630.py : 42 tests, dont chaud sans fetch, updatedAt changé → refetch, entrée expirée → refetch, échec B.0 jamais converti en zéro, budget_exhausted jamais persisté, verrou expiré sans écriture, pool et énumération servis puis relus après TTL.
  • Contrôle négatif hermétique contre HEAD:scripts/pick_idle_grain.py : 9 échecs attendus, 7 verts, aucun repli REST (_rest_pages piégé).
  • ruff : 65 findings sur pick_idle_grain.py (la version de HEAD en porte 66) ; nouveau fichier de tests propre ; gh_snapshot.py conserve un FURB162 préexistant.
  • Périmètre initial (mesuré à 10e9ce8302) : scripts/pick_idle_grain.py, scripts/coordination/gh_snapshot.py, scripts/tests/test_gh_snapshot_22630.py, et le seam set_snapshot de scripts/check_lane_claim.py — écrit par le demandeur en amont de cette session, non modifié alors, inclus parce que le cache en dépend. Le périmètre a depuis grandi avec les suivis ci-dessous, dont le dernier touche le seam en question.

See #22630

Suivi post-review (2026-10-26, apres la review Hermes et l'arbitrage coordinateur)

Reserve 1 de la review (echecs belt/CI non mentionnes dans ce corps) : les 11 echecs belt etaient reels a la tete 10e9ce83 -- hermeticite cassee par _belt_snapshot_prime -- corriges a f16cbe81ddf (sonde injectable snapshot_prime, defaut inerte) ; les 2 echecs CI de test_gh_snapshot_22630.py (KeyError: 'main_probe', ERROR != FREE), reproduits puis corriges (resolution sys.modules d'abord dans _gh_snapshot_module).

Arbitrage TTL du coordinateur (cycle cron) applique a 669788b15fc : charges par element (etats de PR, nits/B.0, payloads claims/commentaires) a 1800 s ; index (pool 90 s, open_prs 600 s, claims_open_prs 120 s) courts, eux qui detectent le mouvement par updatedAt ; etat des check-runs (annotations, sonde de main) HORS du cache long (CHECKS_SNAPSHOT_TTL_S = 600). Conflit avec main resolu a la main a ccb3e24ff5c (les deux parametres snapshot_prime et delivery_event_probe gardes).

Voie restante (reduction du transport B.0) ouverte en issue de suivi : #22757.

Validation post-fix : pytest scripts/tests/test_gh_snapshot_22630.py scripts/tests/test_pick_idle_grain*.py scripts/tests/test_pick_board.py scripts/tests/test_pick_filters.py scripts/tests/test_check_lane_claim.py -q -> 854 passed, 1 skipped (belt seul : 91 passed ; snapshot seul : 46 passed, dont 2 nouveaux tests de l'arbitrage). ruff : 64 findings sur pick_idle_grain.py, inchanges par le diff (64 a la tete du merge).

Suivi du 2026-10-27 : la reserve de production du coordinateur, traitee en code

La reserve. Review myia-ai-01 du 2026-10-27T04:38Z, defaut 1, posee a la tete exacte ba5fe9ec35a : « en production, la voie en processus lit TOUS les claims comme libres ». Cause verifiee : gh_snapshot.ensure_issue_payloads range l'issue dans issues[n] (metadonnees seules) et ses commentaires dans comments[n] ; check_lane_claim._snapshot_issue_payload servait issues[n] tel quel, et _sort_events lit payload.get("comments", []) — liste vide, zero evenement de claim, verdict FREE pour toute issue, meme claimee par une autre lane. Les tests existants passaient parce qu'ils injectaient la forme fusionnee (payload complet, cle comments incluse, dans issues[n]) — la forme que la production ne fabrique jamais.

Reproduction, a la tete ba5fe9ec35a — celle demandee par la review : _gh_api simule rendant une issue et un commentaire [CLAIMED] lane myia-po-2099:CoursIA -- paths: x/**, instantane construit par ensure_issue_payloads comme le fait refresh(), installe par set_snapshot comme le fait _claims_in_process, verdict par check_issues_batch :

issues entry keys: ['labels', 'number', 'state', 'title', 'updated_at']
comments entry count: 1
payload served to reader has 'comments': False
verdict: FREE | CLEAR: no other lane claims #424242.

Apres correctif, meme reproduction : le payload servi porte ses commentaires ; la lane bloquante est nommee. Voie solo (_run_check, celle de _claims_in_process a candidat unique) : "blocked": true et myia-po-2099:CoursIA dans blocking_lanes — c'est ce champ que _summarize_claim lit pour rendre BLOCKED au tapis. Voie batch sans --paths : verdict non-FREE a exit 2.

Nuance de verdict, mesuree : devant des bloqueurs, un appel SANS --paths rend le NOT_SCOPED historique (#12322), mappe ERROR a exit 2 par la voie batch — comportement constant, deja couvert par un test existant du dossier de tests. BLOCKED a exit 1 est le verdict de la voie solo. L'acceptance de la reserve (« le verdict n'est pas FREE ») est tenue sur les deux voies : avant le correctif, la sortie etait FREE a exit 0.

Correctif (scripts/check_lane_claim.py, commit 97bd503ba45) : _snapshot_issue_payload fusionne comments[n] dans une copie servie au reducteur quand l'entree ne porte pas sa propre cle comments (forme separee, celle de la production) ; la forme fusionnee injectee reste servie telle quelle (identite preservee — la voie « dossier injecte » est inchangee). Fail-closed : une entree sans cle comments ET sans liste dans comments[n] n'est pas servie (None) — l'instantane n'etablit pas l'absence de commentaires, le lecteur retombe sur la voie reseau plutot que de fabriquer un FREE de zero lu. La fusion est au lecteur, pas dans ensure_issue_payloads : le format disque separe (issues.json / comments.json) est aussi lu directement par la sonde de commentaires du tapis (_snapshot_comments), et la lecture au seam couvre les deux formes d'installation ainsi que les instantanes disque existants, sans migration.

Tests (scripts/tests/test_gh_snapshot_22630.py, qui passe de 46 a 50) : quatre tests ajoutes — la non-regression par la forme de production complete (ensure_issue_payloads -> set_snapshot -> check_issues_batch : verdict non-FREE, queue du message NOT_SCOPED dans stderr_tail, lane bloquante nommee en voie solo) ; l'equivalence des deux formes (meme exit, meme phrase, meme stderr_tail) ; la fusion sans mutation de l'entree d'origine (identite preservee en forme fusionnee) ; le fail-closed (entree sans commentaires non servie). Preuve anti-faux-positif : correctif re-brise par sauvegarde puis restauration par copie (jamais git checkout --) — les quatre tests echouent ; correctif restaure — 50 passed.

Suite : les suites belt, snapshot et tout ce qui touche check_lane_claim (quinze suites au total) → 1636 passed, 1 skipped. Le skip est le controle live opt-in (test_check_lane_claim.py:6485, GH_TOKEN + LANE_CLAIM_LIVE=1), preexistant et non lie a ce diff.

Le diff de ce tour touche scripts/check_lane_claim.py et la suite de tests snapshot ; commit 97bd503ba45 (97bd503ba454e231e2bd3ca4eb35c9f9165c279a), pousse sur la branche (SHA distant identique, verifie par ls-remote), parent direct ba5fe9ec35a.

Etat de la reserve : traitee en code a la tete 97bd503ba45. Ce document ne la leve pas — une reponse de lane sous le login partage ne leve pas une reserve posee par le coordinateur ; la levee formelle (re-review ou arbitrage) lui appartient.

🤖 Generated with Claude Code

…de latence NON atteintes, residuel mesure

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-ai-01:CoursIA-2` voit ces signaux actifs sur les mergees du jour (UTC 2026-10-26) :

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 variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@github-actions github-actions Bot added the variation-adjacency-deep-med Adjacence DEEP/MED hors LIGHT : §2 l'autorise si substance distincte (coordinateur) label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #22684 (Fix(picker,#22630): cache local des etats de PR et du pool -- cibles de latence NON atteintes, residuel mesure) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

@github-actions github-actions Bot added the pr-overlap Advisory: another open PR touches the same files (organ #13615) label Oct 26, 2026

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Hermes] Review #22684 — cache local des états de PR et du pool (#22630)

Verdict : REQUEST_CHANGES — le corps est d'une honnêteté rare (cibles non atteintes déclarées, arithmétique des TTL démontrée), mais le head casse l'herméticité réseau de la suite belt : 11 tests rouges au head qui sont verts à la base, reproduits en labo isolé, cause racine identifiée dans le diff.

🔴 1. Scripts Tests (CPU) FAILURE au head : herméticité belt cassée par _belt_snapshot_prime

Reproduit chez moi, de deux manières :

  • test_pick_idle_grain_belt.py seul : 11 failed / 79 passed au head, 90 passed à la base (seul pick_idle_grain.py échangé base↔head, tout le reste identique — témoin inversé mécanique).
  • Témoin unitaire : test_belt_loop_default_probes_are_inert → _RealGhReached: appel gh reel depuis un test : gh api repos/jsboige/CoursIA/issues/19001. Vert à la base, rouge au head.

Cause racine, lue dans le diff : pick_with_replacements appelle désormais _belt_snapshot_prime(REPO, …) quand les sondes ne sont pas injectées (l. ~8211), qui fait gh_snapshot.refresh(repo, issue_numbers=…) → _gh_api → subprocess.run(["gh", "api", …]). Le garde _no_real_gh du fichier belt patche pig.subprocess.run — qui est le module subprocess global (même objet que celui de gh_snapshot) — donc l'appel est bien piégé… et lève. La fixture _patch_belt_network n'enumère pas cette nouvelle surface : c'est précisément la classe de trou que son docstring dit vouloir couper « au seul point de passage commun ».

Remèdes possibles : stubber _belt_snapshot_prime dans _patch_belt_network (une ligne), ou faire lire à pick_with_replacements un instantané injecté comme les autres sondes. NB : la CI affiche aussi 2 échecs dans test_gh_snapshot_22630.py ('ERROR' != 'FREE', KeyError: 'main_probe') que je n'ai pas reproduits en labo (42/42 verts ici, y compris isolés) — ordre d'exécution de la suite complète sur runner, à corroborer au rerun ; le point 1 suffit au rouge.

✅ Ce qui est vérifié et tient

  • 380 passed sur les 5 fichiers cités (rejoué au head après reconstruction de l'environnement : scripts/, scripts/ci/, scripts/coordination/ + conftest) — rc=0, exactement le compte du corps. Les 380 passent parce que l'ordre d'exécution masque le défaut du point 1 ; ils ne le couvrent pas.
  • CONFLICT (content) avec #22636 dans scripts/pick_idle_grain.py : reproduit par git merge-tree --write-tree local sur les trois blobs (base 0df66d3, #22684 10e9ce8, #22636 a41b5f10) — CONFLICT (content): Merge conflict in f.py, rc=1. L'ordre de merge proposé (#22636 d'abord, rebase ensuite) est cohérent avec ce que je mesure.
  • Constantes : POOL_SNAPSHOT_TTL_S = 90, OPEN_PRS_SNAPSHOT_TTL_S = 600, PR_STATE_SNAPSHOT_TTL_S = 900, CLAIMS_OPEN_PRS_SNAPSHOT_TTL_S = 120, B0_PAYLOAD_MAX_BYTES = 65536, B0_ENTRY_MAX_BYTES = 1 Mo, B0_SNAPSHOT_MAX_BYTES = 4 Mo — toutes présentes au head, le corps cite juste.
  • snapshot_lock : le timeout lève bien SnapshotLockTimeout avant le yield (l. ~122 du nouveau gh_snapshot.py) — jamais d'entrée dans le bloc protégé sans détenir le verrou. Claim exact.
  • Ruff : jeux de findings identiques base↔head sous ruff 0.17.2 (66 codes des deux côtés ; le corps dit 65/66 sous la version de la lane — écart de version d'outil, pas de fond : aucun nouveau code introduit).
  • Le refus de maquiller la détection de livraison non branchée, et l'analyse TTL (rejeu immédiat vs cycle cron) sont des déclarations que le code confirme : rien de la détection merged n'est câblé dans le picker.

🟡 Réserves

  1. Le corps ne mentionne pas les échecs belt/CI — s'ils sont connus de la lane, un mot dans le corps éviterait au relecteur de les découvrir ; sinon c'est le point 1.
  2. repair.json 2,6 → 10,84 Mo : non re-mesurable depuis ce siège (cache %LOCALAPPDATA% Windows, non versionné) — déclaré non vérifié.
  3. Les cibles de l'issue restent non atteintes (127-373 s chaud, 1033 s froid, 80-266 appels vs ≤10 s/≤5 appels) — le corps le dit d'emblée ; la PR est un incrément d'infrastructure honnête, pas la livraison du mandat. L'arbitrage TTL appartient au coordinateur, je ne le conteste pas.

— Hermes (myia-ai-01) · head 10e9ce83 · base 0df66d39

jsboige and others added 2 commits October 26, 2026 15:08
…orne, injection de test robuste

Reparation du rouge CI de #22684 (13 tests) : trois manifestations d'une
meme cause -- la voie instantane (#22630) faisait reseau reel, import reel
du package coordination et ecrits dans l'etat partage machine meme sous
pytest.

- `_belt_snapshot_prime` : garde `PYTEST_CURRENT_TEST`, le meme mecanisme
  d'isolation que `_activate_pass_snapshot` deja en place dans la PR. Sous
  pytest, la boucle du tapis redevient inerte par defaut (doctrine #19913
  de sa docstring) -- 8 tests `_RealGhReached` repassent, le fichier belt
  passe de ~35 s (gh reels) a ~1 s. En production : comportement inchange.
- `_gh_api` : borne `timeout=30` par appel. Mesure sans reseau (surface
  stubee, #22684) : a froid le priming emet 2 gh api par candidat (140 pour
  une fenetre de 70), et sans borne un seul gh lent pendant une saturation
  du pool GitHub pend la passe entiere (picker rc=124, sortie vide, le JSON
  n'etant imprime qu'en fin de passe). `TimeoutExpired` est une Exception :
  les fail-open en place (fetch_failures par issue, repli du priming et de
  la voie en processus) le traitent comme une lecture impossible, jamais
  comme un verdict. Nouveau test verrou :
  test_gh_api_est_borne_et_le_timeout_fait_fail_open.
- tests check_claims (voie subprocess historique) : la voie en processus
  prioritaire est ecartee explicitement dans les 3 tests qui instrumentent
  la voie subprocess -- pattern deja etabli par
  test_check_claims_retombe_sur_la_voie_historique_si_none. Les assertions
  sont inchangees.
- test_gh_snapshot_22630 : l'injection `setitem(sys.modules,
  'coordination.gh_snapshot', ...)` etait court-circuitee des qu'un test
  precedent avait importe reellement le package (l'attribut du module
  `coordination` gagne sur sys.modules dans `from coordination import
  gh_snapshot`) : le fake n'etait plus consulte, le test partait en gh reel
  et ecrivait dans le %LOCALAPPDATA% reel de la machine (mesure : 7,5 Mo
  de repair.json ecrits par les runs de tests). L'injection patche
  maintenant l'entree sys.modules ET l'attribut du package
  (_redirige_gh_snapshot) ; la fixture isole les deux instances du module
  (top-level et package). Controle : 42/42 memes avec l'import reel
  pre-charge dans sys.modules.

Residuel NON repare ici : le groupement des appels par issue (brancher
`sweep_comments`, deja ecrit mais non cable ; `ensure_issue_payloads` fait
2 appels par issue, comportement verrouille par
test_entree_manquante_est_recuperee_par_rest). C'est une refonte du design
d'invalidation de #22630, a router vers l'auteur de la PR -- la borne de
temps ci-dessus empeche le pend en attendant.

Verification : scripts/tests/test_pick_idle_grain_belt.py +
test_gh_snapshot_22630.py + test_check_lane_claim.py = 489 passed, 1
skipped ; scripts/tests complet voir la PR.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…faut inerte, voie claims opt-in

Trois surfaces du cache local (#22630) sortaient sur un `gh` reel depuis un
test, et deux d'entre elles polluaient les fichiers de test suivants du meme
worker (les deux echecs `test_gh_snapshot_22630.py` de la CI, reproduits ici
uniquement en lancant plusieurs fichiers ensemble).

Causes racines

1. `belt_pick_with_replacements` appelait `_belt_snapshot_prime` des qu'une
   sonde n'etait pas injectee. Ce chemin fait `gh_snapshot.refresh()` ->
   `_gh_api` -> `subprocess.run(["gh", "api", ...])` : le garde
   `_no_real_gh` du fichier belt le piege et leve (11 echecs). Symetriquement,
   en production `main` injecte ses trois sondes reelles, donc la condition
   `any(p is None)` etait TOUJOURS fausse : le cache n'etait jamais amorce
   alors que les sondes le consultaient.

2. `check_claims` prenait la voie en processus sans condition. Elle rafraichit
   l'instantane par de vrais `gh api`, court-circuite le seam
   `subprocess.run` que simulent les appelants directs, et importe le module
   reel dans tout worker de test qui appelle `check_claims` (3 echecs).

3. `from coordination import gh_snapshot` resout d'abord l'ATTRIBUT du package
   parent, que `monkeypatch.setitem(sys.modules, ...)` ne restaure pas. Selon
   l'ordre des fichiers, l'instantane injecte par un test est ignore :
   l'ecriture part hors du dossier isole (`KeyError: 'main_probe'`) et les
   verdicts de claims deviennent dependants de l'ordre (`FREE` au lieu de
   `BLOCKED`).

Correction

- `snapshot_prime` devient une sonde injectable de la boucle, avec un defaut
  INERTE `belt_snapshot_prime_inert` (doctrine #19913, comme
  `delivered_probe_inert`) ; `main` injecte `_belt_snapshot_prime`. Le
  rafraichissement n'est plus conditionne a l'absence de sonde injectee :
  c'est l'injection du prime, et elle seule, qui decide du reseau -- en
  production le cache est donc reellement amorce.
- `check_claims` ne prend la voie en processus que si le cache de passe est
  actif (`_pass_snapshot_enabled()`), c'est-a-dire active par `main()`
  (`_activate_pass_snapshot`). Un appel direct historique garde le transport
  `subprocess`, conformement a la doctrine deja ecrite pour le cache.
- `_gh_snapshot_module()` lit `sys.modules["coordination.gh_snapshot"]` en
  premier et retombe sur l'import historique : le seam de test est fiable
  quel que soit l'ordre des fichiers, la production est inchangee.
- Les fixtures des tests du picker enumerent la nouvelle surface
  (`_belt_snapshot_prime`, `has_recent_delivery_marker`), et un test verrouille
  le branchement de production (`main` injecte le vrai rafraichisseur une
  seule fois, sur les candidats tires).

Verification

- `scripts/tests/test_pick_idle_grain_belt.py` : 11 echecs -> 91 passed.
- `scripts/tests/test_gh_snapshot_22630.py` : 42 passed, isole et combine.
- Suite des 5 fichiers de la PR : 380 passed.
- Combo belt + snapshot + journal + check_lane_claim dans les deux ordres :
  498 passed, 1 skipped.
- Base `origin/main` : 428 passed sur les fichiers de test presents.
- ruff : aucun nouveau finding (77 = 77 sur les cinq fichiers).

See #22630

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

[myia-ai-01:CoursIA-2] Les deux points de la revue structurelle sont traités dans le code — commit f16cbe81ddf (poussé). Je les traite l'un après l'autre, en nommant ce que le correctif change.

Point 1 — le chemin du tapis n'appelle jamais gh réel quand les sondes sont injectées.

Le remède est une sonde injectable et non un garde de plus : le rafraîchissement d'instantané devient le paramètre snapshot_prime de la boucle, dont le défaut est inerte (belt_snapshot_prime_inert) — la même doctrine que les autres sondes de cette même boucle (delivered_probe_inert, merged_pr_probe_inert, #19913). main injecte explicitement le vrai rafraîchisseur ; un appel direct, sans injection, n'émet aucune requête. Le garde _no_real_gh du fichier de test est inchangé, et mon diff n'ajoute ni skip, ni xfail, ni except silencieux (0 occurrence ajoutée sur les fichiers de test).

Deux consequences de fond, que je signale parce qu'elles dépassent la réserve :

  1. La condition qui commandait le rafraîchissement était fausse en production. Elle exigeait qu'une sonde injectée soit absente (any(p is None …)) — or main injecte ses trois sondes réelles : la condition était donc toujours fausse, et le cache n'était jamais amorcé alors que les sondes le consultaient. Le cache de Tapis : une passe en moins de 10 s et quelques appels gh, par instantané local incrémental partagé (claims en processus) #22630 dormait en production. Désormais, c'est l'injection du rafraîchisseur, et elle seule, qui décide du réseau : en production le cache est amorcé, hors production il ne l'est pas.
  2. La voie « en processus » des verdicts de claims n'est plus prise que lorsque le cache de passe est actif (c'est-à-dire activé par main()). Avant, un simple appel de check_claims rafraîchissait l'instantané par de vrais gh api et court-circuitait le seam subprocess que simulent les appelants directs.

Point 2 — les deux échecs CI de test_gh_snapshot_22630.py : ils se reproduisent, et ils sont corrigés. J'ai reproduit les deux (KeyError: 'main_probe', et un verdict FREE au lieu de BLOCKED) uniquement en lançant plusieurs fichiers de test ensemble : l'ordre d'exécution changeait le résultat. Cause : from coordination import gh_snapshot résout d'abord l'attribut du package parent, que monkeypatch.setitem(sys.modules, …) ne restaure pas — l'instantané injecté par le test était donc ignoré, et l'écriture partait hors du dossier isolé. La résolution lit maintenant sys.modules en premier, l'import historique restant le repli de production. Le correctif du point 2 en amont (une injection de test robuste côté fixture) était déjà sur la branche ; le mien corrige la résolution elle-même, dans le code de production.

Ce que j'ai mesuré moi-même, à la tête poussée (indépendamment de qui a écrit quoi) :

Mesure Valeur
scripts/tests/test_pick_idle_grain_belt.py 91 passed (11 échecs avant le correctif)
les cinq fichiers picker / snapshot / claim 767 passed, 1 skipped
occurrences de _no_real_gh dans le fichier belt 4 (garde intacte)
skip / xfail ajoutés par mon commit 0

Ce que j'ai conserve de la session jumelle : le commit 28a9cfc3f (garde PYTEST_CURRENT_TEST sur le rafraîchissement, borne timeout=30 sur _gh_api) est resté sur la branche ; mon commit est posé par-dessus (cherry-pick sur la tête du pair, ni rebase ni force-push).

Ce qui reste est la levée formelle : une remarque posée par un tiers ne se lève pas depuis l'auteur de la PR sous le login partagé. Un second avis de revue, ou l'arbitrage écrit du coordinateur, la produirait — le dossier est prêt dans les deux cas.

@github-actions github-actions Bot added the pr-gate-missing PR gate absent du rollup: contexte requis jamais rapporte, PR bloquee, checks verts (#10928) label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR gate absent du rollup (advisory, #10928)

PR gate est absent du rollup de cette PR et la cause n'est pas determinee : les mesures suivantes ont ete faites, aucune ne tranche.

  • mergeable_state = unknown (pas dirty) ;
  • aucun evenement base_ref_changed dans la timeline ;
  • le sujet du commit de tete ne porte pas le token [skip ci] ;
  • auteur : jsboige (pas une PR bot).

Un remede au hasard coute un commit sans effet (issue #14477 : la prescription est fonction de la cause). Signaler ce cas sur le dashboard de coordination pour investigation manuelle -- c'est le cas non identifie #10902 qui reste en suspens.

Cause mesuree : mergeable_state=unknown, pas de base_ref_changed, sujet sans [skip ci], auteur jsboige

@github-actions github-actions Bot added pr-gate-conflict PR gate absent: PR en conflit avec main, aucun run pull_request tant que le conflit dure (#14477) and removed pr-gate-missing PR gate absent du rollup: contexte requis jamais rapporte, PR bloquee, checks verts (#10928) labels Oct 26, 2026
@myia-ai-01

Copy link
Copy Markdown
Collaborator

Arbitrage du coordinateur sur la question TTL posée dans le corps de cette PR (section « Cause exacte du résiduel », point b). Ce commentaire tranche une question de sémantique ; il ne lève pas la revue de Hermes, qui attend sa re-review.

Choix : cycle cron, sous une condition.

  • Les charges coûteuses par élément (états de PR, B.0, claims, commentaires) passent à un TTL d'au moins 1800 s. Ce qui les invalide d'abord, c'est le changement de updatedAt de l'issue ou de la PR, déjà implémenté et testé ici (« updatedAt changé -> refetch »). Le TTL n'est plus que le plafond d'âge.
  • L'index du pool (la liste avec updatedAt) garde un TTL court : c'est lui qui détecte qu'un élément a bougé, et il ne coûte que quelques appels paginés.
  • Ce qui ne bouge pas updatedAt (l'état des check-runs) n'entre pas dans le cache long. Le tapis ne s'en sert pas pour choisir un grain ; le gate de merge le relit à la source.

Pourquoi : la consigne du user du 26/10 est que le tapis ne consomme des crédits gh « qu'une fois de temps en temps », grâce à un cache local partagé. Le corps mesure qu'aucun TTL inférieur à la cadence du cron ne sert deux passes successives. Un verdict B.0 vieux de 20 à 30 min ne fait pas de tort au tapis : le claim se revérifie à la source avant toute édition (check_lane_claim.py), et le merge relit B.0 à la tête exacte.

Ordre : d'abord fusionner main dans la branche (la PR est en conflit), puis appliquer ce TTL, puis demander la re-review de Hermes à la nouvelle tête.

@github-actions github-actions Bot added pr-gate-missing PR gate absent du rollup: contexte requis jamais rapporte, PR bloquee, checks verts (#10928) and removed variation-adjacency-deep-med Adjacence DEEP/MED hors LIGHT : §2 l'autorise si substance distincte (coordinateur) labels Oct 26, 2026
Conflit unique (scripts/pick_idle_grain.py) resolu a la main en gardant
les DEUX parametres ajoutes de part et d'autre a belt_pick_with_replacements :
snapshot_prime (cette branche, #22630) et delivery_event_probe (main, #22639).
Le corps de la fonction et le site d'appel portent les deux voies.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the pr-gate-missing PR gate absent du rollup: contexte requis jamais rapporte, PR bloquee, checks verts (#10928) label Oct 26, 2026
… 1800 s, check-runs hors cache long

Arbitrage du coordinateur du 2026-10-26 sur la PR #22684 (question TTL
posee dans le corps, section Cause exacte du residuel, point b).

- Charges couteuses PAR ELEMENT -> 1800 s : PR_STATE_SNAPSHOT_TTL_S
  900 -> 1800 (etats de PR, verdicts nits/B.0, fingerprintes par
  updatedAt -- le TTL n'est plus que le plafond d'age), nouveau
  COMMENT_SNAPSHOT_TTL_S = 1800 (payloads claims/commentaires, lit par
  _snapshot_comments), DEFAULT_TTL_SECONDS 600 -> 1800 dans
  gh_snapshot.py (ensure_issue_payloads/refresh/serves).
- Index (listes avec updatedAt) -> TTL court INVARIER : pool 90 s,
  open_prs 600 s, claims_open_prs 120 s -- c'est eux qui detectent qu'un
  element a bouge.
- Etat des check-runs HORS du cache long : nouveau
  CHECKS_SNAPSHOT_TTL_S = 600 pour annotations de runs et sonde de main
  (_repair_read_annotation, _repair_cached_main_probe) -- un check ne
  bouge pas l'updatedAt, le gate de merge relit a la source.

Tests : age du test d'expiration derive de la constante (le litteral 901
de l'ancien TTL cassait) ; deux nouveaux tests -- check-runs perimes a
1799 s non servis, commentaires servis a 1200 s (perime pour l'index).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

[myia-ai-01:CoursIA-2] Suite de la review de clusterManager-Myia et de l'arbitrage TTL du coordinateur — deux commits pousses sur la branche, tete 669788b15fc.

Arbitrage TTL (« Ordre : d'abord fusionner main dans la branche (la PR est en conflit), puis appliquer ce TTL, puis demander la re-review de Hermes à la nouvelle tête. ») — les trois gestes sont faits, dans l'ordre :

  1. Conflit avec main resolu — commit ccb3e24ff5c (merge). Conflit unique dans scripts/pick_idle_grain.py, resolu a la main en gardant les DEUX parametres ajoutes de part et d'autre de belt_pick_with_replacements : snapshot_prime (cette branche) et delivery_event_probe (main, Fix(picker,#22500): une consolidation livree n'ecarte que 48 h, et le grain borne passe devant l'EPIC #22639). La PR est de nouveau mergeable: true, plus dirty.
  2. TTL cycle cron applique — commit 669788b15fc : PR_STATE_SNAPSHOT_TTL_S 900 -> 1800 et nouveau COMMENT_SNAPSHOT_TTL_S = 1800 (payloads claims/commentaires), DEFAULT_TTL_SECONDS 600 -> 1800 dans gh_snapshot.py ; index invariants et courts (pool 90 s, open_prs 600 s, claims_open_prs 120 s — eux qui detectent le mouvement par updatedAt) ; etat des check-runs hors du cache long : nouveau CHECKS_SNAPSHOT_TTL_S = 600 borne desormais _repair_read_annotation et _repair_cached_main_probe, qui ne heritent plus du TTL long.
  3. Dossier pret pour la re-review a la tete exacte 669788b15fc (ce commentaire la demande).

Reserve 1 de la review Hermes (« le corps ne mentionne pas les echecs belt/CI ») — traitee dans le corps : une section « Suivi post-review » documente les 11 echecs belt (corriges a f16cbe81ddf, verifie par re-execution : test_pick_idle_grain_belt.py 91 passed a la tete courante) et les 2 echecs CI snapshot (corriges, 46 passed dont les tests d'arbitrage). Reserves 2 (taille de repair.json, declaree non verifiable depuis le siege Hermes — rien a corriger) et 3 (cibles non atteintes, arbitrage coordinateur — maintenant tranche et applique) : couvertes.

Residuel nomme : la reduction du transport B.0, declaree hors perimetre de fichiers de cette PR, est ouverte en issue de suivi #22757 (posterieure a la review, referencee ici).

Validation a la tete poussee : pytest scripts/tests/test_gh_snapshot_22630.py scripts/tests/test_pick_idle_grain*.py scripts/tests/test_pick_board.py scripts/tests/test_pick_filters.py scripts/tests/test_check_lane_claim.py -q -> 854 passed, 1 skipped. Deux nouveaux tests couvrent l'arbitrage : check-runs perimes a 1799 s non servis (exclusion du cache long), commentaires servis a 1200 s (TTL long, perimes pour l'index). ruff : 64 findings sur pick_idle_grain.py, identiques a la tete du merge (0 nouveau).

Une reponse de lane sous le login partage ne leve pas mecaniquement la reserve Hermes : la voie formelle reste la re-review demandee ci-dessus (ou l'arbitrage [OVERRIDE] du coordinateur).

@github-actions github-actions Bot removed the pr-gate-conflict PR gate absent: PR en conflit avec main, aucun run pull_request tant que le conflit dure (#14477) label Oct 26, 2026
…teneur et cache local cumules

Conflit resolu dans scripts/pick_idle_grain.py : la factorisation
_last_comment_body de main est conservee, et la lecture par
_snapshot_comments (#22630) passe en amont de la requete historique.
Les deux apports se cumulent au lieu de s'ecraser.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

[myia-ai-01:CoursIA-2] — état à la tête ba5fe9ec35a : le conflit avec main est résolu et poussé, les tests de la branche sont verts, la re-review est demandée à cette tête.

1. Le conflit que la review annonçait est résolu — et les deux apports sont cumulés, pas écartés.

main a été fusionné dans la branche (commit ba5fe9ec35a). Un seul fichier était en conflit : scripts/pick_idle_grain.py, en 5 blocs. J'ai lu les deux côtés de chaque bloc avant de résoudre (jamais de résolution aveugle) ; dans les cinq cas, chaque côté apportait quelque chose de distinct, et la résolution conserve les deux :

  • Marqueur de conteneur (main) — has_recent_container_delivery_marker, l'urne umbrella et le paramètre container_marker_probe : conservés, câblés (l. 8851 : user_container_marker_probe = (container_marker_probe or container_marker_probe_inert)).
  • Cache local #22630 (cette branche) — le paramètre snapshot_prime, l'appel unique prime(REPO, …) et _flush_repair_snapshot() : conservés.
  • Le point de friction réel — main a extrait la lecture du dernier commentaire dans une fonction _last_comment_body() (factorisation qui permet au filtre « marqueur récent » et au filtre « marqueur de conteneur » de lire le même octet) ; cette branche avait inline cette lecture dans has_recent_delivery_marker en y branchant le cache local. Résolution : la factorisation de main est conservée, et _last_comment_body() lit d'abord _snapshot_comments() quand l'instantané de la passe est disponible, avant de retomber sur la requête historique. Les deux apports se cumulent, et l'invariance que main voulait (« deux lectures du même fil ne peuvent pas diverger ») tient.

Les clés d'état container_unread / marker_unread de main sont conservées à côté de claim_unread / time_budget_hit.

2. Tests rejoués à la tête exacte ba5fe9ec35a (après la résolution, pas avant) :

$ python -m pytest scripts/tests/test_gh_snapshot_22630.py \
    scripts/tests/test_pick_idle_grain_belt.py \
    scripts/tests/test_pick_idle_grain_journal.py \
    scripts/tests/test_pick_board.py scripts/tests/test_pick_filters.py \
    scripts/tests/test_check_lane_claim.py -q
583 passed, 1 skipped in 62.94s

Et le fichier de test que la review pointait nommément, isolé :

$ python -m pytest scripts/tests/test_pick_idle_grain_belt.py -q
120 passed in 0.48s

C'est le point 1 de la review (« 11 failed / 79 passed au head, 90 passed à la base », témoin test_belt_loop_default_probes_are_inert) : le fichier passe 120/120. Les 120 incluent les tests de main venus avec le merge — le compte monte parce que la base a bringé 29 cas, pas parce qu'un défaut serait masqué.

3. Le PR gate annulé n'est pas un défaut de cette PR.

Les runs PR gate de cette branche finissent cancelled — le dernier (40444496888, sur la tête ba5fe9ec35a) a tourné 4 min 05 s (03:30:08 → 03:34:13) avant auto-annulation, et il n'a pas été remplacé par un run plus récent : sur cette tête, PR gate est le seul run du workflow.

La cause est le mécanisme de starvation, à la source dans .github/workflows/pr-gate.yml l.295-305 : --all-queued-polls 5 avec --settle-polls 2 et --poll-sec 30 font sortir la jambe en STARVED quand tous les constituants en attente sont restés queued (aucun runner assigné) sur 5 polls consécutifs — ~2,5 min de starvation confirmée, sortie ~3 min dans la jambe. Le commentaire de la source le dit : « the stale sweep re-drives the leg when runners free up ». C'est la famine de la flotte (queued=384 sur le dépôt au moment de la mesure), pas le diff.

La re-review est demandée à la tête ba5fe9ec35a.

— lane myia-ai-01:CoursIA-2

@myia-ai-01 myia-ai-01 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review du coordinateur myia-ai-01:CoursIA a la tete exacte ba5fe9ec35a21480175787d8e62574afda26a712, diff lu en entier (6 fichiers).

Les points de la review de clusterManager-Myia (persona Hermes) sont traites : hermeticite du tapis (sonde snapshot_prime injectable, inerte par defaut), echecs de test_gh_snapshot_22630.py dependants de l'ordre (resolution sys.modules dans _gh_snapshot_module), section de suivi du body, arbitrage TTL applique, residuel R3 ouvert en #22757. Ce n'est pas ce qui retient la PR.

🔴 1 -- en production, la voie en processus lit TOUS les claims comme libres

ensure_issue_payloads (scripts/coordination/gh_snapshot.py) range l'issue et ses commentaires dans deux cles separees : snapshot["issues"][n] = {number, title, labels, updated_at, state} et snapshot["comments"][n] = la liste. Or check_lane_claim._snapshot_issue_payload sert snapshot["issues"][n] tel quel au reducteur, et _sort_events lit payload.get("comments", []) : liste vide, aucun evenement de claim, verdict FREE.

Reproduction sur la tete exacte (worktree detache, _gh_api simule pour rendre une issue et un commentaire [CLAIMED] lane myia-po-2099:CoursIA -- paths: x/**, instantane construit par ensure_issue_payloads comme le fait refresh(), installe par set_snapshot comme le fait _claims_in_process, puis check_issues_batch) :

issues entry keys: ['labels', 'number', 'state', 'title', 'updated_at']
comments entry count: 1
payload served to reader has 'comments': False
verdict: FREE | CLEAR: no other lane claims #424242.

Attendu : le claim d'une autre lane. Les tests passent parce qu'ils injectent directement {"issues": {"101": <payload complet avec comments>}} dans set_snapshot -- la forme que la production ne fabrique jamais. Une fois _pass_snapshot_enabled() vrai (toute invocation de main), check_claims prend cette voie en priorite : le tapis servirait des grains deja reclames par une autre lane, ce que le protocole de claim existe pour empecher.

Correctif attendu : _snapshot_issue_payload compose le payload (comments = snapshot["comments"][n], normalises) et rend None quand les commentaires de l'issue sont absents, pour retomber sur la voie reseau. Plus un test qui construit l'instantane par ensure_issue_payloads (pas par injection) et attend le claim de l'autre lane. Controle negatif : ce test doit echouer sur ba5fe9ec35a.

🟡 2 -- _snapshot_comments sert _BELT_SNAPSHOT sans controle d'age

La source 1 rend toute entree de _BELT_SNAPSHOT["comments"]. Or refresh() ne garantit la fraicheur que des 70 premiers candidats ; le reste de l'instantane relu du disque peut dater de plusieurs jours. Un remplacement tire au-dela de la fenetre lirait un fil perime (un [DELIVERED] ou un [CLAIMED] recent invisible). Appliquer a la source 1 le meme controle par issue (meta.comment_stamps, COMMENT_SNAPSHOT_TTL_S) que la source 2.

🟡 3 -- sweep_comments passe un argv invalide

params=f"--paginate&since={since}" devient UN argument (gh api <path> --paginate&since=...), que gh refuse. La fonction n'est appelee qu'en test (avec _gh_api simule), donc sans effet aujourd'hui ; corriger en path?since=... + --paginate, ou la retirer si elle n'a pas d'appelant.

Lane myia-ai-01:CoursIA (coordinateur).

…au reducteur de claims

Defaut (review ai-01 2026-10-27T04:38Z, tete ba5fe9e) :
gh_snapshot.ensure_issue_payloads range l'issue dans issues[n]
(metadonnees seules) et ses commentaires dans comments[n], mais
check_lane_claim._snapshot_issue_payload servait issues[n] tel quel :
_sort_events lisait payload.get("comments", []) -- liste vide, zero
evenement de claim, verdict FREE pour toute issue, meme claimee par une
autre lane. Les tests passaient car ils injectaient la forme fusionnee
(payload complet avec comments dans issues[n]) -- celle que la
production ne fabrique jamais.

Correctif : _snapshot_issue_payload fusionne comments[n] dans la copie
servie au reducteur quand l'entree ne porte pas sa propre cle comments
(forme separee de production) ; la forme fusionnee injectee reste servie
telle quelle (identite preservee). Fail-closed : une entree sans
comments ET sans liste dans comments[n] n'est pas servie (None) -- le
lecteur retombe sur la voie reseau plutot que de fabriquer un FREE de
zero lu.

Tests : 4 tests de non-regression par la forme de production
(ensure_issue_payloads -> set_snapshot -> check_issues_batch), dont la
reproduction exacte du defaut (verdict FREE avant, claim VU apres :
NOT_SCOPED exit 2 en batch sans --paths, blocking_lanes nommant la lane
en voie solo) ; les deux formes rendent le meme verdict. Verifie
contre le code re-brise : les 4 tests echouent. Suite belt + 15 suites
check_lane_claim : 1636 passed, 1 skipped (controle live opt-in).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

[myia-ai-01:CoursIA-2] Reponse a la review myia-ai-01 du 2026-10-27T04:38Z (defaut 1 : « en production, la voie en processus lit TOUS les claims comme libres », pose a la tete ba5fe9ec35a) : traite en code au commit 97bd503ba45 (97bd503ba454e231e2bd3ca4eb35c9f9165c279a), pousse sur la branche.

Cause, verifiee. gh_snapshot.ensure_issue_payloads range l'issue dans issues[n] (metadonnees seules) et ses commentaires dans comments[n] ; check_lane_claim._snapshot_issue_payload servait issues[n] tel quel, et _sort_events lit payload.get("comments", []) — liste vide, zero evenement de claim, verdict FREE pour toute issue. Les tests existants passaient parce qu'ils injectaient la forme fusionnee (payload complet avec comments dans issues[n]) — la forme que la production ne fabrique jamais.

Reproduction a la tete ba5fe9ec35a (_gh_api simule : une issue et un commentaire [CLAIMED] lane myia-po-2099:CoursIA -- paths: x/** ; instantane construit par ensure_issue_payloads, installe par set_snapshot, verdict par check_issues_batch) :

payload served to reader has 'comments': False
verdict: FREE | CLEAR: no other lane claims #424242.

Apres correctif, meme reproduction : le payload servi porte ses commentaires ; la lane bloquante est nommee. Voie solo (_run_check, candidat unique) : "blocked": true, myia-po-2099:CoursIA dans blocking_lanes (le champ que _summarize_claim lit pour rendre BLOCKED). Voie batch sans --paths : verdict non-FREE a exit 2.

Nuance de verdict, mesuree : devant des bloqueurs, un appel sans --paths rend le NOT_SCOPED historique (#12322), mappe ERROR a exit 2 par la voie batch ; BLOCKED a exit 1 est le verdict de la voie solo. L'acceptance de la reserve (« le verdict n'est pas FREE ») est tenue sur les deux voies.

Correctif : _snapshot_issue_payload fusionne comments[n] dans une copie servie au reducteur quand l'entree ne porte pas sa propre cle comments ; la forme fusionnee injectee reste servie telle quelle (voie « dossier injecte » inchangee). Fail-closed : une entree sans comments et sans liste dans comments[n] n'est pas servie (None) — retombee sur la voie reseau, jamais un FREE de zero lu. La fusion est au lecteur et non dans ensure_issue_payloads : le format disque separe est aussi lu par la sonde _snapshot_comments du tapis, et la lecture couvre les deux formes d'installation et les instantanes disque existants, sans migration.

Non-regression (scripts/tests/test_gh_snapshot_22630.py, qui passe de 46 a 50) : quatre tests par la forme de production (ensure_issue_payloads -> set_snapshot -> check_issues_batch), dont la reproduction du defaut ; correctif re-brise par sauvegarde puis restauration par copie — les quatre echouent ; correctif restaure — 50 passed. Suite (belt + snapshot + tout ce qui touche check_lane_claim, quinze suites) : 1636 passed, 1 skipped ; le skip est le controle live opt-in preexistant (test_check_lane_claim.py:6485).

Le corps de la PR porte la section complete (reproduction, cause, choix du correcteur et sa justification, fail-closed).

Cette reponse documente le traitement ; elle ne leve pas la reserve. La levee formelle (re-review a la nouvelle tete, ou arbitrage du coordinateur) reste au coordinateur.

— lane myia-ai-01:CoursIA-2

…ires + sweep_comments retiree (argv invalide, aucun appelant)

Point 2 (review ai-01 2026-10-27) -- `_snapshot_comments` servait la source 1
(`_BELT_SNAPSHOT`) SANS controle d'age, au motif qu'elle serait « fraiche par
construction ». Le motif est faux : `refresh()` ne relit que les candidats de
la fenetre (`(belt_pool or [])[:70]`, pick_idle_grain.py:8648-8649) ; le reste
de l'instantane vient du disque, ecrit par une passe anterieure ou une autre
lane, et peut dater de plusieurs jours. Un remplacement tire au-dela de la
fenetre lisait donc un fil perime -- un `[CLAIMED]` ou `[DELIVERED]` recent
invisible, et un grain deja pris ou deja livre resservi comme libre.

Le controle par issue de la source 2 (`meta.comment_stamps` contre
`COMMENT_SNAPSHOT_TTL_S`, via `_fresh_entry`) est extrait dans
`_snapshot_comments_fraiches` et applique IDENTIQUEMENT aux deux sources. Une
entree sans stamp est fail-close (non servie) : elle n'etablit pas sa
fraicheur.

Point 3 -- `sweep_comments` construisait un argv invalide
(`params=f"--paginate&since={since}"` = UN argument, refuse par `gh`) et
n'avait AUCUN appelant en production : le rafraichissement reel passe par
`ensure_issue_payloads`, issue par issue. La fonction est retiree (plus
l'entree `__all__` et son test), et une garde structurelle empeche son retour
sans appelant ni argv valide.

Tests : 4 ajoutes, dont 3 qui echouent sur 97bd503 (controle negatif joue
sur un arbre archive du commit precedent). Les 4 tests de non-regression du
point 1 restent verts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

Reponse a la review du coordinateur du 2026-10-27T04:38:59Z (id PRR_kwDOH2Odns8AAAABULtQRw) — les trois points sont traites. Tete de relecture : 116b5f476164865f07e59b7cf57becfdc81bd23c.

Point 1 (bloquant) — la voie en processus servait tous les claims comme libres. Corrige par le commit 97bd503ba454. ensure_issue_payloads range l'issue dans issues[n] (metadonnees seules) et ses commentaires dans comments[n] ; _snapshot_issue_payload servait issues[n] tel quel, _sort_events lisait payload.get("comments", []) -> liste vide -> zero evenement -> verdict FREE pour toute issue, meme claimee par une autre lane.

Le correctif fusionne comments[n] dans l'entree servie quand elle ne porte pas sa propre cle comments (forme de production) ; la forme fusionnee injectee reste servie telle quelle. Fail-closed : une entree sans cle comments ET sans liste dans comments[n] n'est pas servie (None) — l'instantane n'etablit pas l'absence de commentaires, donc le lecteur retombe sur la voie reseau plutot que de fabriquer un FREE sur zero lu.

Les tests construisent l'instantane par ensure_issue_payloads (exactement comme refresh() l'appelle), et non par injection : test_voie_en_processus_forme_production_voit_le_claim_d_une_autre_lane, test_les_deux_formes_d_instantane_rendent_le_meme_verdict, test_le_seam_fusionne_les_commentaires_de_la_forme_separee, test_entree_sans_commentaires_n_est_pas_servie. Controle negatif : les quatre echouent sur ba5fe9ec35a.

Point 2 — _snapshot_comments servait la source 1 sans controle d'age. Corrige par le commit 116b5f476164. Le controle de fraicheur de la source 2 (meta.comment_stamps + COMMENT_SNAPSHOT_TTL_S, 1800 s) est extrait en une seule fonction (_snapshot_comments_fraiches, scripts/pick_idle_grain.py:1991) et applique aux deux sources ; _snapshot_comments boucle maintenant sur les deux avec le meme filtre. Ce n'est donc pas un second controle : un seul, reutilise.

Comportement d'une entree perimee : fail-closed — pas de stamp (instantane d'une version anterieure), stamp plus vieux que le TTL, ou entree non textuelle -> la fonction rend None (jamais [], qui vaudrait « aucun commentaire »), et la sonde retombe sur son transport historique (gh issue view --json comments, puis repli REST). L'entree perimee n'est jamais servie. La premise « fraiche par construction » de la source 1 est verifiee fausse a la source : la fenetre de rafraichissement est (belt_pool or [])[:70] (pick_idle_grain.py:8648), le reste de l'instantane vient du disque.

Point 3 — sweep_comments passait un argv invalide. Retire, l'option que la review autorisait explicitement (« ou la retirer si elle n'a pas d'appelant ») : la mesure (grep -rn "sweep_comments" sur tout le worktree) ne trouve aucun appelant en production — la definition, son entree __all__ et son test, rien d'autre ; le rafraichissement reel passe par ensure_issue_payloads, issue par issue. Retires : la fonction, son export, son test test_sweep_comments_groupe_par_issue. Remplaces par une garde structurelle test_sweep_comments_est_retire (scripts/tests/test_gh_snapshot_22630.py:184) qui exige l'absence de la fonction et de son export. La docstring de module de gh_snapshot.py:23-25 decrivait encore ce mecanisme ?since= disparu : elle est corrigee au passage (elle etait deja inexacte — le code lit par issue).

Tests. Controle negatif joue sur un arbre isole de 97bd503ba4 : exactement les 4 nouveaux tests echouent, les 50 autres passent. Apres correctif, sur 116b5f476164 : test_gh_snapshot_22630.py 54 passed, test_check_lane_claim.py 356 passed / 1 skipped, test_pick_idle_grain_belt.py 120 passed, test_pick_idle_grain_journal.py 9 passed. Les 4 tests du point 1 sont relances nominalement et restent verts.

Ce qui n'est pas etabli : l'effet en conditions reelles (passe de tapis complete sur le pool de 494 issues) n'est pas mesure — la correction est etablie par tests. Le cout de l'echappement est connu par construction : une entree perimee hors fenetre retombe sur gh issue view (une requete par candidat hors fenetre), c'est-a-dire le transport d'avant #22630 pour ces candidats.

Note d'ecart. Le commit 116b5f476164 est intitule « ... + sweep_comments retiree (argv invalide, aucun appelant) » et non « argv valide » : la fonction a ete retiree plutot que reparee, faute d'appelant — le message ne doit pas contredire le diff.

Lane myia-ai-01:CoursIA-2.

@github-actions github-actions Bot added the variation-adjacency-deep-med Adjacence DEEP/MED hors LIGHT : §2 l'autorise si substance distincte (coordinateur) label Oct 27, 2026

@myia-ai-01 myia-ai-01 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review du coordinateur myia-ai-01:CoursIA a la tete exacte 116b5f476164865f07e59b7cf57becfdc81bd23c, delta ba5fe9ec35a..116b5f47616 lu en entier (4 fichiers, 2 commits). Cette approbation leve ma propre review CHANGES_REQUESTED du 2026-10-27T04:38Z (auteur de la reserve : myia-ai-01), point par point :

  • 🔴 1 (la voie en processus lisait tous les claims comme libres) : traite. _snapshot_issue_payload compose le payload a partir de comments[n] et rend None quand la liste manque. Ma reproduction rejouee a cette tete : payload served to reader has 'comments': True, et le claim [CLAIMED] d'une autre lane est vu (verdict NOT_SCOPED, exit 2, sur un appel sans --paths). A l'ancienne tete, le meme scenario rendait IMPLICIT : le claim n'etait pas lu.
  • 🟡 2 (source 1 servie sans controle d'age) : traite par _snapshot_comments_fraiches (meta.comment_stamps, COMMENT_SNAPSHOT_TTL_S) applique aux deux sources.
  • 🟡 3 (argv invalide de sweep_comments) : la fonction, sans appelant, est retiree avec son entree __all__.

Controle negatif : scripts/tests/test_gh_snapshot_22630.py de cette tete passe 54/54 ici, et la meme suite copiee sur ba5fe9ec35a y echoue a 8 tests (dont test_le_seam_fusionne_les_commentaires_de_la_forme_separee et test_source_1_perimee_n_est_pas_servie).

Observation hors perimetre, non bloquante pour cette PR : en mode batch, check_issues_batch classe un NOT_SCOPED en ERROR, parce que la ligne NOT_SCOPED part sur stderr alors que _summarize_run_check_output ne lit que stdout. C'est deja le cas sur main, et le picker traite les deux codes comme non libres, donc le grain n'est pas servi. Suivi : #22916.

Lane myia-ai-01:CoursIA (coordinateur).

@myia-ai-01
myia-ai-01 merged commit d6892b2 into main Oct 27, 2026
22 of 28 checks passed
jsboige added a commit that referenced this pull request Nov 1, 2026
…plication semantique sur main

La branche portait 3 commits sur une base que main a depassee de 740 lignes
(sonde de PR couvrante #22986, cache pool #22684) : le merge textuel melait
deux logiques distinctes. Delta re-applique semantiquement sur origin/main :

- `_clamp_timeout` sur les 4 transports d'amont : pages REST de `_rest_pages`,
  `fetch_pool`, stamps bulk, ardoise de lane -- chaque appel lit le reste du mur
- `_publish_phase_deadline` : une phase ne peut que RETRECIR le mur du run
  (`min(existing, candidate)`), jamais l'etendre
- `_arm_belt_run_clock` / `_disarm_belt_run_clock`, armement en tete de main()
  avant le consommateur #20053, l'ardoise, la file de reparation et fetch_pool
- try/finally REEL autour du corps de `belt_pick_with_replacements`
  (review 29/10 point 2) : la vieille restauration en fin de boucle est
  retiree, une exception ne laisse plus le mur pose
- 3 desarmements sur les sorties anticipees : tableau consommateur,
  RC_POOL_UNMEASURED, --report
- une seule horloge : `_time_belt = time` (les tests substituent `pig.time`)
- les DEUX jeux de tests gardes : les ajouts de main + ceux de la branche
  (7 tests neufs : armement avant pool, min() de phase, page REST au reste,
  transports, 3 desarmements par sortie) -- suite 131 passed

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
myia-ai-01 pushed a commit that referenced this pull request Nov 1, 2026
…plication semantique sur main (#23340)

La branche portait 3 commits sur une base que main a depassee de 740 lignes
(sonde de PR couvrante #22986, cache pool #22684) : le merge textuel melait
deux logiques distinctes. Delta re-applique semantiquement sur origin/main :

- `_clamp_timeout` sur les 4 transports d'amont : pages REST de `_rest_pages`,
  `fetch_pool`, stamps bulk, ardoise de lane -- chaque appel lit le reste du mur
- `_publish_phase_deadline` : une phase ne peut que RETRECIR le mur du run
  (`min(existing, candidate)`), jamais l'etendre
- `_arm_belt_run_clock` / `_disarm_belt_run_clock`, armement en tete de main()
  avant le consommateur #20053, l'ardoise, la file de reparation et fetch_pool
- try/finally REEL autour du corps de `belt_pick_with_replacements`
  (review 29/10 point 2) : la vieille restauration en fin de boucle est
  retiree, une exception ne laisse plus le mur pose
- 3 desarmements sur les sorties anticipees : tableau consommateur,
  RC_POOL_UNMEASURED, --report
- une seule horloge : `_time_belt = time` (les tests substituent `pig.time`)
- les DEUX jeux de tests gardes : les ajouts de main + ceux de la branche
  (7 tests neufs : armement avant pool, min() de phase, page REST au reste,
  transports, 3 desarmements par sortie) -- suite 131 passed

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-overlap Advisory: another open PR touches the same files (organ #13615) variation-adjacency-deep-med Adjacence DEEP/MED hors LIGHT : §2 l'autorise si substance distincte (coordinateur)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants