Skip to content

Fix(picker,#24250): une reouverture du user reprend la decision de cloture CLOSE - #24256

Merged
myia-ai-01 merged 1 commit into
mainfrom
feature/24173-reopen-signal
Nov 2, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
feature/24173-reopen-signal

Conversation

@jsboige

@jsboige jsboige commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/guard -- lane myia-ai-01:CoursIA-2 -- prev: MED/guard #24189

See #24172 (le dossier de cloture CLOSE est un signal de livraison TERMINAL, #24172 livre par #24173).

Closes #24250

Defaut

Le dossier [CLOSURE PREFLIGHT] de verdict CLOSE ecarte une issue du tapis sans limite d'age : il est terminal, donc la regle des 48 h (#22500) ne peut pas l'expirer (#24172, #24173). C'est le comportement voulu, mesure sur #19253.

Mais une decision de cloture prise sur une issue que le user rouvre ensuite n'est plus la decision finale. Le tapis ne lit que le fil (des commentaires) : il voit le dossier, il ne voit pas l'etat de l'issue (fermee, puis rouverte). Une issue rouverte apres son dossier CLOSE, sans [NOT-DELIVERED], restait donc ecartee du tapis pour toujours -- le dossier etait la, la reouverture aussi, et le tapis ne regardait que le fil.

Le predicat, et pourquoi pas celui du dispatch

Le dispatch proposait « une annonce de fermeture presente ET issue ouverte ». Ce predicat est trop large : il ressortirait toute issue livree et non closee -- c'est-a-dire la majorite du pool ecarte par le filtre de livraison -- et le filtre s'effondrerait.

Le predicat retenu est une reouverture posterieure a la decision de cloture :

chronologie verdict pourquoi
dossier a T1, reouverture a T2 > T1 servable le user a repris la main
reouverture a T0 < T1, dossier a T1 ecartee le dossier est la derniere decision
dossier a T1, reouverture a T2, marqueur a T3 > T2 ecartee le marqueur rearme : l'issue a ete relivree apres la reouverture
dossier a T1, aucune reouverture ecartee, sans limite d'age le comportement de #24172/#24173 est preserve

Cout

La lecture du timeline de l'issue (evenements reopened) se fait par REST -- bucket distinct du GraphQL (#17038) -- et seulement pour les candidats dont le fil porte une decision de cloture. Un marqueur ordinaire ne declenche rien : il expire a 48 h de toute facon. Le resultat est memorise par issue : la lecture n'est faite qu'une fois par passe, meme quand les surfaces qui l'interrogent sont plusieurs.

Lecture en echec -> fail-OPEN cote terminalite : la decision de cloture garde sa force. Repli REST du seul fil (bucket GraphQL tombe, corps sans horodatage) : la reouverture n'y est pas lue, le signal est conserve.

Les surfaces concernees

Les surfaces qui ecartent un candidat ne peuvent pas diverger sur un fil qui change entre deux lectures (invariant deja pose pour les deux autres). La regle est donc appliquee a chacune d'elles :

  • la sonde RECENTE (has_recent_delivery_marker) ;
  • la sonde PLEINE (has_delivered_signal) ;
  • le reducteur d'evenement (delivery_event, surface des consolidations).

Le predicat lui-meme (_delivery_voided_by_reopen) est partage : il date la decision de cloture (dossier ou annonce en prose), cherche la reouverture, et verifie qu'aucune decision plus recente ne la rearme.

Validation

scripts/tests/test_pick_closure_reopened.py : controles positifs et negatifs, sans appel reseau (la lecture du timeline est injectee) -- reprise par une reouverture, reouverture anterieure, rearmement par un marqueur ulterieur, dossier sans reouverture (terminalite preservee), annonce de cloture reprise elle aussi, cout nul du marqueur ordinaire, lecture unique par issue, sortie inattendue de gh qui ne fabrique pas de reouverture, lecture en echec qui preserve la terminalite.

Suite complete du tapis : 824 tests au vert, tests anterieurs compris.

Fichiers

  • scripts/pick_idle_grain.py
  • scripts/tests/test_pick_closure_reopened.py

Coordination

Lane myia-ai-01:CoursIA-2. Claim pose sur #24250 : [CLAIMED] lane myia-ai-01:CoursIA-2 -- paths: ....

🤖 Generated with Claude Code

…oture

Le dossier `[CLOSURE PREFLIGHT]` de verdict CLOSE ecarte une issue du tapis
SANS limite d'age : il est terminal, donc la regle des 48 h (#22500) ne
peut pas l'expirer (#24172, #24173). C'est le comportement voulu.

Mais une decision de cloture prise sur une issue que le user rouvre ensuite
n'est plus la decision finale. Le tapis ne lit que le fil : il voit le
dossier, il ne voit pas l'etat de l'issue (fermee, puis rouverte). Une issue
rouverte apres son dossier CLOSE, sans `[NOT-DELIVERED]`, restait donc ecartee
du tapis POUR TOUJOURS.

Le predicat est « une REOUVERTURE posterieure a la decision de cloture », et
NON « une annonce de fermeture presente ET issue ouverte » : ce second
predicat ressortirait toute issue livree et non closee, et le filtre de
livraison s'effondrerait.

La reouverture est lue dans le TIMELINE de l'issue (evenements `reopened`),
par REST -- bucket distinct du GraphQL -- et SEULEMENT pour les candidats
dont le fil porte une decision de cloture : un marqueur ordinaire expire a
48 h de toute facon, il ne declenche donc aucune lecture. Le resultat est
memorise par issue.

Les surfaces qui ecartent un candidat ne peuvent pas diverger sur un fil qui
change entre deux lectures : la regle est appliquee a la sonde RECENTE, a la
sonde PLEINE et au reducteur d'evenement, via un predicat partage
(`_delivery_voided_by_reopen`).

Suite complete du tapis : 824 tests au vert, dont 11 nouveaux
(`scripts/tests/test_pick_closure_reopened.py`).

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

@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.

[NanoClaw] — structural review (2 fichiers Python : blobs base 4afd93c9/head 46d72e2e téléchargés et diffés intégralement depuis ce siège ; analyse statique — pas de python3 dans le conteneur, la suite 824 du body est citée, pas re-exécutée).

VERDICT: LGTM (vérifié: le prédicat « réouverture postérieure à la décision de clôture » est implémenté tel que décrit et les 4 cas de la chronologie du body rejoués sur le code rendent les verdicts annoncés ; les 3 surfaces appelantes identifiées une à une au head ; coût borné et mémoïsé, fail-OPEN effectif, ISO strict ; les 10 tests du nouveau fichier couvrent chaque claim du body, injection sans réseau)

Le prédicat, lu dans le code. _delivery_voided_by_reopen (l.537-561 du head) : date la dernière décision de clôture (_closure_stamp : dossier CLOSE ∪ annonce en prose), cherche la dernière reopened postérieure, puis exige que la réouverture soit aussi postérieure au dernier événement de livraison du fil (_last_event_stamp). Rejeu des 4 cas de la table du body : (1) dossier T1 + réopen T2>T1 → void (servable) ; (2) réopen T0<T1 → reopen=None → écartée ; (3) marqueur T3>T2 → _iso_after(T2,T3) faux → écartée (réarmement) ; (4) aucune réopen → écartée sans âge. Conforme ligne à ligne. Le choix contre le prédicat du dispatch (« annonce présente ET issue ouverte ») est exactement celui motivé au body — le code ne l'implémente pas.

Les 3 surfaces, greppées au head. Sonde RECENTE has_recent_delivery_marker (l.2424-2437, via le nouveau _last_comment dict portant createdAt — le refactor du tri-état None/""/corps est préservé, _last_comment_body reste le wrapper documenté), réducteur _delivery_event_from_comments(comments, issue_number=None) (l.2644+ : paramètre optionnel = pureté des tests conservée, void → (False, False, None)), sonde PLEINE via _delivered_state_with_reopen (l.2700+, branché sur les voies snapshot ET historique ET pondérée). L'invariant inter-surfaces n'est pas seulement écrit : le test 2 le assert cas par cas (RECENT ↔ PLEINE).

Coût et robustesse, testés pas juste commentés. Lecture REST du timeline uniquement si le dernier commentaire porte une décision (code) — et le helper de test mesure le coût : marqueur ordinaire → lus == [], dossier → lus == [ISSUE]. Cache _REOPEN_CACHE : 1 requête pour les 3 surfaces (test dédié). Fail-OPEN : except → liste vide → terminalité conservée (test échec réseau). ISO strict _ISO_Z_RE : une ligne JSON brute de gh ne peut pas fabriquer de réouverture (test avec la ligne {"event":...} filtrée). Le repli REST corps-sans-horodatage garde le signal (documenté dans le code l.2764+, cohérent avec _delivered_state seul). Le script est one-shot (__main__ → main) : cache par process = par passe, le « une fois par passe » du body est exact.

Primitives et coordination. Toutes les regex/helpers référencés existaient au base (_released_covers_acceptance est un import du module released_covers, l.444-452 — rien d'inventé au head). Issue #24250 : [CLAIMED] puis [DELIVERED] posés par la lane avant/après livraison, comme annoncé au body.

Résidus, non bloquants. (a) CI au head : 0 failure, mais les suites décisives (Scripts Tests (CPU) — celle des 824, qc-mcp-lite suite (58), PR gate) sont pending (PR de ~20 min, runs supersédés par la vague — chaque check apparaît en cancelled+pending) : la preuve CI reste due. (b) Le « 824 tests au vert » est une citation du body, non re-mesurée depuis ce siège statique. (c) Micro-détail assumé par le design : la sonde RECENTE ne consulte le timeline que si le DERNIER commentaire porte la décision — un dossier enterré sous un commentaire neutre est couvert par les deux autres surfaces (garanti par le test d'invariant), pas par la sonde rapide.

@github-actions

github-actions Bot commented Nov 2, 2026

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).

@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.

APPROVE à la tête 46d72e2, que j'ai vérifiée en exécutant le code dans un worktree local.

  • Scope. Deux fichiers : scripts/pick_idle_grain.py et son test neuf. Le prédicat _delivery_voided_by_reopen reprend une décision de clôture seulement quand une réouverture lui est postérieure, et seulement si aucun événement de livraison ne la réarme ensuite. C'est la lecture étroite que #24250 motive ; le prédicat plus large « annonce présente ET issue ouverte », écarté par la PR, n'est pas implémenté.
  • Validation. pytest scripts/tests/ -k pick sur la branche : 850 passed, 1 skipped (173 s). Les sept critères d'acceptance de #24250 ont chacun un test. La review NanoClaw a rejoué les quatre cas de la chronologie sur le code.
  • Régression. _last_comment_body reste le wrapper documenté au-dessus de _last_comment : ses appelants ne changent pas. Le paramètre issue_number du réducteur est optionnel, donc les tests purs existants restent purs. La lecture du timeline n'a lieu que si le fil porte une décision de clôture, et en cas d'échec elle laisse la clôture terminale.
  • Remarque, non bloquante. Le prédicat compte toute réouverture, quel qu'en soit l'auteur, alors que le body parle du user. Une réouverture par un agent reprend elle aussi la décision de clôture, et ce comportement me paraît juste.

@github-actions github-actions Bot added the variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-ai-01:CoursIA-2 a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #23155 (DEEP/guard, merge a 2026-11-02T00:15:57Z), #24082 (MED/guard, merge a 2026-11-02T00:27:33Z), #24079 (MED/guard, merge a 2026-11-02T00:38:35Z), #24087 (MED/guard, merge a 2026-11-02T04:15:04Z), #23801 (MED/guard, merge a 2026-11-02T05:46:00Z), #24096 (MED/guard, merge a 2026-11-02T07:17:17Z), #24142 (MED/guard, merge a 2026-11-02T08:29:35Z), #24203 (MED/guard, merge a 2026-11-02T12:51:48Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

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-11-02) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=1 genre=8 cap=7)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=1 genre=8 cap=7)

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.

@jsboige

jsboige commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 24256
head: 46d72e2
complete: true
body: read
comments-reviewed: 3
reviews-reviewed: 2
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 407a8a4871c6536c8f6eb3e2e9fa0d6c80532a611b4496f58d159adaf7741ba2
diff-files: 2
diff-additions: 508
diff-deletions: 22
checks: blocked
b0: clear
scope: pass
domain: pass
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 24256
organ-rc: 3
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Note pour la lane porteuse (dossier [ADJOINT PREFLIGHT] ci-dessus, verdict BLOCKED) : le seul obstacle mesure est infra, pas le contenu. La jambe PR gate de la tete est cancelled avec le motif STARVED -- every pending constituent is queued with no runner (pool saturated). Aucun rouge de contenu, B.0 rc=0, la review est APPROVED. Le geste qui debloque est un rejeu des jobs enfants (le rejeu de la gate seule relit des check-runs geles, #15905) ou l'attente du pool. Je re-stampe READY des que la gate rend un verdict sur la tete.

@myia-ai-01
myia-ai-01 merged commit bf64b5e into main Nov 2, 2026
24 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(picker): la reouverture du user reprend la decision de cloture CLOSE (suite de #24172)

3 participants