Dotfiles_ArchAP/docs/session-v2-login-focus-investigation.md

16 KiB

Premier login V2 — investigation isolée du 17 octobre 2026

Cette page conserve l'investigation précédente et son état BLOCKED historique. La suite phase 0 prewarm et focus final implémente la préparation avant transaction autorisée par le nouveau ticket, sans relaxer Recovery. La reproduction précédente du refus naturel reste disponible ; ses résultats précèdent ce changement.

Résultat et portée

BLOCKED pour une restauration automatique prouvée du premier login. La cause initiale du journal réel fourni ne peut pas être reconstruite à partir de son observation tardive. Le patch est conservateur : il préserve le premier écart observé, bloque aussi les mutations prédites neutres après conflit et vérifie l'identité runtime à la réadmission. Il ne transforme pas un mapping admis en preuve de provenance du focus. Ne pas présenter ce patch comme une résolution du login ou le déployer automatiquement.

Travail limité à /tmp/labfy-session-v2-real-focus-PFRt9hSW, état Git initial sans modification. Aucun fichier réel du journal n'a été recherché. Les données réelles ci-dessous viennent exclusivement du ticket. Aucun client utilisateur, autostart, catalogue Limusic, service, socket graphique utilisateur ou journal RECOVERY_REQUIRED utilisateur n'a été modifié. Aucun commit/push.

Faits, hypothèse et chaîne d'exécution

Le checkpoint fourni contient Firefox/Kitty horizontalement en 1, Limusic seul en 10 et focus=None. Au login, focus_origin={con_id:null,workspace:"1"} est une preuve de workspace explicite, différente d'un focus indisponible {con_id:null,workspace:null}. Les trois clients sont observés, trois ancres créées/nettoyées, aucune permutation effectuée. Le cleanup a pu écraser le premier écart : la transition Limusic5 → Firefox13 associée à kill de l'ancre8 ne prouve donc pas le déclencheur initial.

Ordre effectif des sources de cette copie :

  1. Executor.apply vérifie deux arbres et la session, prend le lock local, puis _apply fait preflight, crée Recovery et la baseline. Le lock ne verrouille ni l'input utilisateur, ni les autres clients IPC.
  2. Anchors.__init__ lance le helper GTK, sans encore créer de fenêtre. Guard découvre/suspend autotiling avant de répondre ready. Cela ne constitue pas une barrière générale du compositeur ni des autres processus.
  3. stage_node admet les clients existants puis émet leur déplacement via RecordedSway → Recovery.command. Pour le scénario fourni, la première mutation d'un client est le déplacement de Limusic existant de 10 au stage. Le cas d'un client déjà actif peut changer le focus à cet instant ; aucune nouvelle application n'est encore lancée. Déplacer le dernier client peut aussi faire disparaître le workspace 10 et compliquer le recovery ultérieur.
  4. Anchors.build sélectionne les workspaces et outputs par IPC journalisé, puis écrit create sur le stdin du helper. La première création d'ancre est la première admission de nouvelle surface susceptible de prendre le focus sans passer par RecordedSway.command. Les focus/layout suivants sont journalisés. L'exception existante anchor_owned utilise PID/pidfd et identités exactes du helper ; ce n'est pas une preuve de cause de l'input.
  5. L'executor sélectionne le stage puis appelle launch. Les Popen sont séquentiels, leurs mappings sont asynchrones : ordre de lancement différent d'ordre de mapping. Limusic reçoit toujours zéro lancement. Les autres clients peuvent prendre le focus pendant l'attente des cardinalités.
  6. Les groupes sont constitués par app_id. Le provider Firefox résout les UUID avant l'admission Recovery des nouveaux clients. capture/resolve lit l'arbre et publie une requête runtime ; le companion change titlePreface, sans demander focused:true. Le native host ne commande pas Sway. Une UUID exacte ne prouve pas qui a changé le focus pendant l'attente du provider.
  7. La boucle captured admet les identités puis stage les clients non présents dans le stage. La première commande suivante peut constater le focus modifié. Avant le patch, même en conflit, une commande prédite sans changement de focus pouvait partir ; le refus sur une permutation ou un workspace était tardif. Le guard nettoie ses ancres et relit Recovery, puis l'executor relit à son tour Recovery. Plusieurs observateurs pouvaient remplacer le même diagnostic.

Hypothèse plausible, non prouvée pour le login : un map de nouveau client ou une transition liée aux ancres explique le premier écart, particulièrement avec un workspace initial sans feuille. Le nombre de fenêtres ou l'absence de swap ne suffit pas à discriminer un map, un input, un helper ou une commande IPC.

Garanties et limites externes

Le manuel IPC Sway décrit les réponses de commande, GET_TREE et les événements de fenêtre. Les informations focused, focus, id, pid décrivent l'état, pas une autorité causale de transaction. Ni GET_TREE suivi de RUN_COMMAND ni une souscription aux événements ne fournissent un compare-and-swap atomique du focus avec preuve de sa cause. Un tick peut aider à ordonner les observations ; il n'exclut pas l'input.

Dans Sway 1.11, view.c, should_focus/view_map, une surface mappée dans l'espace actif peut recevoir le focus. Le premier client d'un espace vide peut même contourner no_focus. C'est un mécanisme plausible, pas la preuve du comportement exact du SwayFX installé (0.6, basé sur 1.12.0). Une règle globale no_focus [app_id=...] ne constitue donc pas la correction.

Le protocole layer-shell distingue absence d'intérêt clavier, exclusivité et activation à la demande ; l'interactivité est appliquée au commit de surface. Deux observations QML de retrait ne constituent pas une barrière Sway ni la garantie qu'aucune autre layer ne réapparaisse. Le focus workspace explicitement validé ne doit pas être remplacé par une feuille tirée de la pile inactive.

Autotiling amont réagit aux événements IPC et émet des commandes de split. La suspension vérifiée des helpers connus est utile ; elle n'annule pas une commande déjà soumise et n'empêche pas le démarrage d'une nouvelle instance après découverte. Aucun de ces mécanismes n'établit l'origine d'un changement de focus du lot restauré.

Patch et régressions

Recovery.record_focus_conflict publie dès le premier constat et conserve cette observation avec setdefault ; les chemins avant/après commande, refus de préservation et entrée recovery l'utilisent. La phase indique où l'écart a été observé, pas son événement causal. Les anciens journaux restent lisibles, sans migration ni réécriture de leur observation historique ; ce qui a été écrasé ne peut pas être retrouvé rétroactivement.

Après focus_conflict, check_focus_preservation refuse toute nouvelle commande mutante du journal, même prédite neutre. Cela peut laisser davantage de clients au stage qu'auparavant : le recovery partiel n'est plus un moyen de continuer après une ambiguïté. Le guard conserve la responsabilité d'arrêter son propre helper et de reprendre ses helpers suspendus ; il ne tue jamais les clients.

admit vérifie désormais session, con_id, PID, app_id et starttime aussi lors d'une réadmission. L'origine épinglée ne change jamais. Cette vérification ne résout pas la première admission par app_id ni l'intervalle entre lecture et commande IPC ; aucune permission de focus ou de déplacement n'est ajoutée.

Les régressions inertes couvrent workspace sans feuille, mapping tardif de Firefox/Kitty après stage de Limusic existant, refus sans nouvel IPC, préservation du premier écart malgré cleanup/relecture, changement de session/PID/starttime/ app_id, faux app_id d'ancre et mutation neutre après focus tiers. Les tests existants couvrent également le crash mid-IPC avec intention pending. La simulation des postconditions utilise le prédicteur existant : ce n'est pas une vérification indépendante des règles du compositeur.

Validation et reproduction proposée sur l'hôte

Résultats locaux : 23 tests focus/recovery, 27 tests backend, 12 scénarios QML PySide6 et 5 contrôles QtTest passent. La validation du schéma de la nouvelle fixture et git diff --check passent également. Le runner QML doit être celui de Qt6 (/usr/lib/qt6/bin/qmltestrunner) ; le lanceur générique a échoué sans sortie. Trois nouvelles régressions ont aussi été exécutées contre Recovery de HEAD dans une copie de test : elles échouent avant patch (dont émission de la commande interdite après focus tiers et réadmissions non vérifiées), puis passent avec le patch.

Les preuves locales sont sous build/real-focus-env/ (logs et results.json, ainsi que baseline-regressions.log). Les HOME/XDG/runtime de validation sont privés dans cette copie ; les variables de sockets et de bus utilisateur sont retirées. Le SwayFX installé échoue avant le démarrage du serveur : drmGetDevices2 failed, Cannot create GLES2 renderer: no DRM FD available. Aucun résultat fonctionnel headless n'est revendiqué.

Nouvelle fixture composée tests/session_v2/login_focus.py : vrais Executor/Recovery/Guard/Anchors, Limusic synthétique déjà présent en 10, Firefox et Kitty synthétiques absents, workspace 1 sans feuille et checkpoint focus=None. Elle remplace les lancements applicatifs par des surfaces GTK possédées, et le provider par une UUID synthétique stable. Le contrôleur focalise explicitement le nouveau client juste après map et avant retour du lancement. L'assertion attend un refus avant le premier swap, conserve les trois clients et vérifie l'unicité de Limusic. La découverte des helpers utilisateur est neutralisée ; aucune validation d'autotiling réel n'en découle.

À exécuter côté hôte dans cette copie, avec un environnement temporaire fourni par ce harnais (les tests ne doivent jamais hériter d'un socket réel) :

python3 -B tests/session_v2/validate_login_focus.py

Le harnais lance les tests Python, QML et la fixture composée. Il retourne un code non nul si un contrôle échoue, y compris au démarrage headless ; un refus du renderer est une validation indisponible, pas un PASS fonctionnel. Pour exécuter la fixture seule, reprendre les variables privées et les suppressions de variables sockets/bus définies dans ce script. La fixture crée ensuite elle-même un nouveau Lab et utilise exclusivement son socket enfant explicite. Un autre compositeur headless compatible peut être sélectionné par LABFY_TEST_SWAY absolu et LABFY_TEST_RENDERER=pixman, sans remplacer le compositeur installé.

Le résultat attendu est un refus sûr, pas un restore réussi. Compléter ensuite sur hôte par les variantes suivantes : focus naturel sans injection, ancre au map, input tiers avant/après map, nouveau client concurrent de même app_id, réapparition d'une layer, workspace10 disparu, multi-output, mort du helper, crash avant/après IPC et réutilisation PID/starttime/con_id. Chaque variante doit avoir un runtime neuf et garder son arbre avant/après et son premier diagnostic. Même réussie, une telle expérience ne valide pas le vrai login.

Stratégie testable restant bloquée

Ne pas ajouter d'exception « focus sur app_id autorisé » ou « con_id dans le lot ». Épingler session/con_id/PID/starttime est nécessaire mais ne suffit pas à décider si l'utilisateur a focalisé ce même client. Le PID du Popen Firefox peut aussi être un lanceur court relayé vers un processus existant.

Une solution candidate devrait séparer découverte/mapping et mutations de placement, avant de déplacer Limusic, puis exiger une frontière d'autorité explicite et vérifiable : identité de chaque surface, preuve de lancement quand applicable, et contrôle des transitions de focus sur l'autorité du compositeur. Une trace d'événements bornée et les identités épinglées permettraient de tester les hypothèses ; elles ne suffisent pas seules à autoriser une divergence. Une API compositeur conditionnelle avec génération et provenance serait une piste plus forte, hors du petit patch actuel. Une autre politique nécessiterait un acquittement utilisateur explicite après mapping, avec un protocole séparé et testé ; ce n'est pas implicitement autorisé par le clic initial Restaurer.

Décision en attente : choisir une stratégie capable de prouver cette frontière avant toute exception. En attendant, garder les refus et les journaux. Ni les données du ticket ni les API actuellement exploitées ne permettent de revendiquer une réparation automatique sûre du scénario réel.

Procédure DRY-RUN proposée pour le client déjà bloqué — non exécutée

  1. Ne pas relancer restore, ne pas choisir une action qui efface le diagnostic, ne pas tuer/déplacer Limusic, ne pas le relancer. Préparer une inspection avec l'utilisateur ; aucun de ces accès à sa session n'est autorisé par ce ticket.
  2. Après autorisation distincte d'inspection, conserver des copies privées des journal/reçu/rapport et leur empreinte avant toute action. Vérifier leurs transaction_id, session et stage concordants ; ne pas déduire l'appartenance au lot du préfixe du workspace. Relever intention, pending, recovered, focus_origin/expected/conflict et la première observation disponible.
  3. Établir explicitement le socket et l'identité du compositeur de cette session (UID, chemin, device/inode/ctime utilisés pour le hash Sway.session). Relever get_tree/get_outputs/get_workspaces avec ce socket explicitement choisi. Comparer à la session du journal ; si elle diffère, STOP, aucune commande.
  4. Pour chaque con_id candidat, comparer PID, /proc/PID/stat starttime, app_id et session avec origin/expected. Vérifier le PID vivant et son UID, ainsi que l'absence de réutilisation/ambiguïté. App_id ou titre seul ne vaut jamais identité. Une intention pending interdit une application mécanique de l'instruction suggérée dans le reçu.
  5. Construire un tableau sans exécution : identité complète, état observé, origin, expected, workspace/output actuels, workspace/output désirés, flottement/géométrie, statut pending et écarts. Vérifier le contenu du stage, les clients tiers et l'existence des outputs actifs. Vérifier où se trouve l'espace10 : absent, déjà utilisé, ou déplacé d'output. Ne pas supposer que 10 est libre ni que tout client du stage appartient à la transaction.
  6. Proposer uniquement les mouvements nominativement justifiés par ce tableau, avec critères con_id explicitement issus de l'arbre et conditions de refus. Faire approuver explicitement par l'utilisateur ce plan précis, y compris une éventuelle création d'espace10, son output et un changement de focus. Sans cette autorisation, DRY-RUN terminé, zéro mutation.
  7. Après une éventuelle autorisation future, refaire immédiatement les preuves session/PID/starttime/emplacement/outputs/focus avant chaque action. Tout écart ou concurrence annule le plan. Ne jamais faire de boucle app_id, kill client, lancement de Limusic, focus de feuille devinée ou rollback forcé.
  8. Vérifier ensuite toutes les identités et emplacements demandés, le contenu du stage, les outputs, les clients tiers et le focus final convenu. Conserver avant/après et résultats nominatifs. Un stage encore occupé ou une preuve manquante reste RECOVERY_REQUIRED.
  9. Archiver seulement après ces preuves et accord explicite, avec copies, empreintes, session/transaction et reçu de récupération manuelle. Ne pas transformer un échec en succès automatique, ni simplement supprimer le journal pour débloquer le bouton. Aucune de ces étapes n'a été exécutée ici.