14 KiB
Session V2 — focus et récupération, 22 octobre 2026
Portée et état
Patch isolé sur fix/session-v2-focus-recovery-20261022, worktree
/home/fy59/Documents/Development/labfy-session-v2-focus-recovery-20261022,
base 5aeff41d2ce8c91966ce65fd2fa3261f34f3f954. Le dépôt principal était propre
à cette base. Aucun commit, déploiement, redémarrage ou IPC mutant sur le bureau.
Les tests utilisent des arbres inertes ou des compositeurs enfants headless privés.
Les trois journaux réels ont été lus et leurs SHA-256 vérifiés inchangés.
Le choix réel startup.json=new ne donne aucune autorisation de reprise.
La fausse classification d'un changement de chemin est reproduite et corrigée pour les nouveaux journaux disposant d'une preuve de structure. L'ordre de fin du helper d'ancres est corrigé et les frontières du watchdog sont journalisées. La cause historique exacte du focus Firefox → Kitty reste non démontrée. Ce patch ne constitue donc pas une preuve de correction définitive du login réel. Aucune divergence n'est réinterprétée comme un succès.
Chronologie établie de l'incident
Le rapport réel indique trois applications observées, trois slots remplis,
trois ancres créées/permutées/nettoyées, un helper suspendu/repris et l'arbre
vérifié. final_tree_verified=true est une étape validée, pas la preuve que
l'arbre est resté identique après reprise.
Le journal conserve operations=18, une dernière intention [con_id=8] focus,
focus_expected={con_id:8,workspace:"1"} et focus_pending=false. Dans le code
exécuté, ces deux derniers champs ne sont publiés qu'après le retour réussi de
RUN_COMMAND et une observation de focus conforme. Cela prouve une observation
de Firefox après la commande ; cela ne prouve pas l'absence d'input concurrent.
La séquence source était :
- Verrou V2 et refus d'une récupération active/ambiguë ; prewarm des applications, identités/PID/starttime, puis preuve de focus stable sur deux arbres.
- Création de Recovery et baseline, lancement du helper d'ancres sans surface, découverte et suspension vérifiée d'autotiling via pidfd.
- Réadmission stricte du lot, déplacements au stage, construction des ancres, résolution Firefox, permutations et retrait des seules ancres possédées.
- Vérifications de structure, identités, outputs et flottants ; choix du focus final depuis le checkpoint ou l'origine prouvée ; commande vers Firefox8.
guard.resume()envoie SIGCONT et attend seulement l'état non stoppé du helper._verify()refait notamment l'appel provider etvalidate_final(). Sa lecturefinal-observationconstate Kitty7, enregistre la première divergence et échoue.- L'appel normal suivant à
guard.finish()n'a donc pas encore été atteint. Le finally appelle ensuite finish sur erreur, puis la récupération conservatrice.
Le premier conflit after-command/final-observation désigne une validation,
pas une commande focus qui aurait échoué. La fenêtre causale est postérieure
à l'observation conforme de Firefox et antérieure à cette lecture. Elle contient
la reprise des helpers et l'attente provider. L'incident ne permet pas de choisir
entre un helper, un autre client IPC, une activation Wayland/layer ou un input
utilisateur. Il ne démontre pas un défaut du déplacement initial.
Autotiling, Sway et interface
L'autotiling installé est lancé par exec autotiling dans l'autostart Sway.
Son handler switch_splitting traite WINDOW/MODE, relit le focus puis envoie
splith/splitv sans critère con_id. SIGCONT peut donc libérer un backlog et une
commande déjà préparée ; l'ACK actuel ne prouve aucun drainage. Le code ne demande
pas directement de focus. Les splits peuvent insérer un parent unaire ; le code
Sway de container_split conserve le focus de son enfant lorsqu'il était focalisé.
La seule présence d'autotiling ne démontre donc pas la transition vers Kitty.
Une restauration avec le véritable autotiling dans un Lab privé réussit avant
et après patch.
Les erreurs réelles 'NoneType' object has no attribute 'type' sont visibles
à 06:11:58.214 et 06:11:58.390, mais aussi avant à 06:11:53.835/.836. Le handler
appelle output_name(con) avant de vérifier con : un find_focused() nul peut
produire précisément cette erreur. Il n'existe ni traceback ni trace de commandes
ou d'input permettant d'en déduire une commande de focus causale.
Les règles Sway du dépôt ne refocalisent pas Firefox/Kitty. Le script historique
layout-workspace1.sh contient des commandes focus sur d'autres identités, sans
invocation dans l'autostart examiné. L'abonné d'opacité commande l'opacité et les
bordures. Le provider Firefox change titlePreface, sans focused:true ; un
événement titre peut toutefois réveiller autotiling. L'UI retire sa layer avant
le backend et vérifie le retrait sur deux ticks ; cela ne constitue pas une
barrière atomique sur tous les clients Wayland. startup.py n'effectue aucun IPC
mutant et le choix new ne déclenche aucune reprise. Ces composants restent inchangés.
Références primaires : IPC Sway 1.12,
autotiling.
Les sources locales SwayFX sway/tree/container.c, sway/input/seatop_default.c
et sway/ipc-server.c ont aussi été inspectées. RUN_COMMAND/GET_TREE séquentiels
ne sont pas un compare-and-swap du focus et ne fournissent pas sa provenance.
Protocole corrigé
Après le nettoyage/vérification du layout, l'executor observe le focus,
puis appelle guard.prepare(). Le watchdog refuse s'il reste une ancre et
quitte son helper ; il acquitte seulement après preuve de sortie sur le pidfd.
Cela se produit pendant la suspension possédée, avant le focus final et SIGCONT.
La vérification Anchors.owned commence par PID/app_id : les fenêtres applicatives
peuvent être vérifiées après la sortie/récolte du helper, sans dépendre d'un zombie.
Une candidate possédée exige toujours son pidfd exact.
Le moteur contrôle le focus après prepare, émet la commande finale sous les contrôles Recovery existants, observe avant/après resume, puis avant/après finish. La clôture préparée ne ferme plus d'ancre et n'écrit plus de quit tardif. Les chemins d'erreur conservent le nettoyage des seules ancres possédées et la reprise de tous les helpers dont le watchdog possède la suspension. Un helper initialement stoppé n'est jamais repris.
Les six observations focus_boundaries sont durables et bornées à huit ; elles
localisent un premier constat, sans attribuer une cause. L'intention conserve
désormais explicitement focus_after après réponse et observation conformes.
Toute divergence refuse la suite ; aucun rattrapage/refocus automatique n'est
ajouté. Les contrôles finaux après reprise et après finish restent présents.
Limite : prepare est une barrière sur le processus possédé, pas un ACK coopératif d'autotiling. Un helper repris peut toujours commander Sway. Une nouvelle instance non découverte, un autre client ou l'input restent concurrents. Le patch contrôle ces divergences observables et les refuse ; il ne prétend pas établir une causalité atomique ou empêcher tout changement après la dernière lecture.
Récupération canonique
L'ancien _recover exigeait current == expected, chemins bruts inclus.
Il pouvait donc annoncer IDENTITY_OR_MEMBERSHIP_CHANGED pour [0] → [0,0]
ou [1] → [1,0], alors que la validation finale ignorait déjà ces chemins et
contrôlait ordre/placement/layout. La reproduction inerte échoue avant patch
avec exactement deux rejets Firefox/Kitty, puis passe après correction.
Les nouveaux journaux V1 contiennent une extension facultative
structure_expected, capturée sur l'arbre confirmé après commande. Elle utilise
la même normalisation qu'Anchors : seuls les splits unaires splith/splitv/none
sont contractés. Les feuilles sont les vrais con_id ; branches ordonnées,
layouts tabbed/stacked et appartenance aux workspaces restent significatifs.
La preuve est bornée en profondeur, nombre de branches et workspaces et soumise
à la limite durable JSON existante. Aucune version de Project DB n'est concernée.
Avant de réconcilier des chemins, Recovery exige :
- Même session et aucune intention fenêtre/focus pending.
- Ensemble exact des fenêtres gérées et étrangères, sans admission par app_id.
- Égalité complète con_id/PID/starttime/app_id, workspace/output, flottement et géométrie flottante pertinente pour tout le lot.
- Égalité de la structure canonique à la preuve durable antérieure.
Un changement de branche/layout/ordre reste refusé même si les chemins bruts
n'ont pas changé ; chaque commande de récupération recontrôle la structure.
Seuls les chemins expected sont réconciliés. Origin, before, intention et
premier conflit restent conservés ; path_reconciliations garde les chemins
avant/après (quatre lots maximum, puis refus plutôt qu'écrasement).
Le contrôle strict avant chaque commande relit toujours l'état exact.
Les journaux historiques restent lisibles mais sans fabrication rétroactive de
preuve structurelle. Une différence de chemin sans preuve devient
PATH_EQUIVALENCE_NOT_PROVEN, une vraie différence de placement/identité reste
IDENTITY_OR_MEMBERSHIP_CHANGED, une structure différente devient
STRUCTURE_OR_ORDER_CHANGED. Aucun journal réel n'a été réécrit.
Reconnaître sans IPC une fenêtre déjà à son origine n'efface pas le conflit de
focus : le reçu reste RECOVERY_REQUIRED avec FOCUS_RECOVERY_NOT_PROVEN.
Ce patch ne débloque donc pas automatiquement la transaction réelle déjà refusée.
Validation et preuves
Les commandes, résultats détaillés et empreintes sont conservés dans
build/focus-recovery/, build/real-focus-env/ et build/session-v2-tests/ du
worktree. Ces artefacts sont locaux, non déployés. Commande de validation composée :
LABFY_TEST_RENDERER=pixman python -B tests/session_v2/validate_login_focus.py
Le runner impose HOME/XDG privés et retire les sockets/bus de la session réelle. Chaque Lab lance SwayFX 0.6 / Sway 1.12 sur un socket enfant distinct avec Pixman. Les applications et UUID Firefox sont synthétiques. Les injections « helper » utilisent une commande déterministe du contrôleur après reprise ; elles ne prétendent pas reproduire une commande focus du véritable autotiling.
Les scénarios positifs exigent success, slots/permutations complets, arbre exact,
focus hors stage, recovery complete et toutes les postconditions. Les négatifs
exigent un refus, le maintien des clients et de l'incident, et l'absence de
refocalisation. Les tests d'interruption conservent les intentions pending.
test_bootstrap.py utilise désormais un temporaire court pour ne pas dépasser
la limite AF_UNIX dans un worktree profond ; aucun test n'a été supprimé.
Résultats du dernier contrôle composé : 29 tests focus/recovery, 71 tests
backend, 13 tests QML Python, 5 contrôles QtTest et 15 scénarios de login headless.
Les cinq variantes de path_stability_runtime.py et les dix variantes de
crashes.py passent également. Les scénarios de succès exigent toutes les
postconditions ; les injections tardives exigent RECOVERY_FOCUS_MISMATCH après
les trois swaps, sans refocalisation.
Un élargissement à runtime.py a validé les 28 permutations de layouts ainsi
que les cas de performance/multi-workspace précédant le flottant. La suite
complète ne passe pas : sa fixture GTK flottante repasse de 400×240 à 122×82
après le resize, d'où un refus de postcondition/état. La sonde privée identique
échoue aussi contre la baseline non modifiée (floating-baseline.log) ; le
patch échoue avec le même retour spontané de taille (floating-patched.log).
Aucune tolérance géométrique ni suppression de ce test n'a été ajoutée. Ce
résultat ne permet pas de revendiquer une validation complète des flottants ;
les contrôles continuent de refuser leur géométrie divergente. Les deux cas
d'identité après le flottant dans cette suite n'ont pas été atteints.
Validation proposée au prochain login — autorisation humaine requise
- Faire relire le diff ; autoriser séparément son intégration/déploiement et un essai réel au prochain login. Rien de cela n'a été exécuté par cette mission.
- Conserver les journaux/empreintes de l'incident actuel. Un traitement de son
RECOVERY_REQUIREDnécessite un plan nominatif et une autorisation distincte ; ne pas l'effacer ni utiliser le choixnewpour le reprendre. - Au login autorisé, relever versions, checkpoint voulu, session/socket, outputs et helpers ; utiliser uniquement le choix Restaurer explicite, une seule fois. Pour le cas nominal, laisser les interactions se terminer avant la transaction.
- Conserver tentative/journal/reçu, six frontières focus et arbres avant/après. Vérifier identité runtime de chaque fenêtre, slots/ordre/layout/outputs, ancres absentes, helpers repris, focus convenu hors stage et recovery complete.
- En cas d'écart, arrêter l'essai et conserver son premier constat. Ne pas cliquer Réessayer en boucle, refocaliser via script, relancer apply ou supprimer RECOVERY_REQUIRED. Comparer les frontières et compléter par une trace IPC/input explicitement autorisée pour discriminer les causes restantes.
- Un essai séparé avec interaction volontaire doit conserver le focus extérieur et refuser toute mutation après le conflit observable ; il demande lui aussi une autorisation explicite. Un résultat headless ne valide pas le login physique.