# Session Restore V2 État de cette révision : **deux P0 de frontière prewarm/finalisation corrigés ; validation privée Sway 1.12/Pixman ; login physique non testé pour ce patch**. Le prewarm antérieur a été validé sur SwayFX 0.6 headless ; cette preuve historique ne valide pas automatiquement le présent delta. Lors du premier login du 17 octobre 2026, l’ancienne séquence a échoué avant le premier swap avec `FOCUS_RECOVERY_NOT_PROVEN` : les trois fenêtres ont été remises à leur place manuellement et l’incident archivé. La correction actuelle prépare les applications manquantes avant la transaction et restaure le focus final. Son efficacité lors d’un prochain login physique reste à vérifier. La recette réelle réussie du **7 octobre 2026** est une preuve historique de la révision d’alors : logout, checkpoint V2, arrêt UWSM, login, chooser unique, arbre, focus final et identité Firefox exacte vérifiés. Elle ne valide pas les changements de focus actuels ni le prochain login physique. Les preuves historiques restent dans la branche locale de recherche. ## Utilisation Le menu power conserve Verrouiller, Suspendre, Déconnexion, Redémarrer et Éteindre. Déconnexion sauvegarde un checkpoint V2 avant `uwsm stop` ; Redémarrer et Éteindre sauvegardent avant leur action systemd. Un échec de checkpoint permet d’annuler ou de choisir explicitement de quitter sans sauvegarder. Au login suivant, le chooser propose Restaurer ou Nouvelle session. Un clic Restaurer retire sa surface et attend deux observations QML de ce retrait (délai maximal de 1,5 seconde). Sous le verrou Session V2, le backend refuse d’abord une récupération antérieure inachevée, puis **prépare les applications absentes via le catalogue**, avant de déplacer une fenêtre ou de publier un journal Recovery/Attempt. Limusic appartient exclusivement à l’autostart Sway : elle n’est jamais lancée par Session V2, et son absence bloque la transaction. Lorsque toutes les fenêtres attendues sont présentes, le backend vérifie les identités, emplacements et cardinalités, puis exige une preuve stable du focus Sway et de l’identité de session avant de reconstruire le layout. Un focus inconnu ou ambigu (`con_id=null, workspace=null`) reste refusé sans feuille inventée. Un focus explicitement situé sur un workspace, même sans feuille, est une observation valide, sans preuve de la provenance d’une activation ultérieure. Les changements de focus pendant la transaction restent refusés. Après `DEFERRED` pour absence de focus, le dialogue reste sans capture clavier : **activez vous-même une fenêtre ou un espace avec vos raccourcis Sway, puis cliquez Réessayer**. Si la préparation n’a pas ouvert une application, le dialogue conserve les fenêtres déjà lancées et propose également **Réessayer**, sans les lancer une deuxième fois. Le clic demande une nouvelle preuve ; il n’autorise pas un re-focus arbitraire. Aucun retry n’est automatique. **Nouvelle session** reste disponible. Si une autre surface exclusive conserve le clavier, il faut d’abord la libérer. Un retrait QML expiré permet également un retry explicite sans capture clavier. Le lot phase0 épingle les `con_id`, PID, `/proc` starttime, `app_id` et placements runtime. Après l’initialisation de Guard/Anchors et avant le premier move, le lot entier est relu. Chaque staging vérifie encore les membres et les placements attendus ; ses cibles viennent exclusivement du lot épinglé. Une fenêtre ajoutée ou remplacée après prewarm est refusée, y compris à `app_id` et cardinalité identiques. L’origine Recovery n’est jamais redécouverte par nom applicatif. Aucun lancement Limusic n’est ajouté. La vérification finale relit la structure **après** les appels au provider et **après** `guard.finish()`. Elle contrôle layout canonique, membership, sorties, flottement/géométrie flottante, focus verrouillé et identités runtime complètes de toutes les fenêtres gérées. Les UUID exacts du provider restent exigés. `Recovery.complete()` refuse identité ou placement divergents, ordre relatif modifié, focus pending/conflict et toute fenêtre pending. Executor lui fournit le contrôle structurel sur sa dernière observation avant publication. Une normalisation de parent unaire par Sway reste permise si placement, ordre et layout canonique sont conservés ; les ratios exacts restent hors contrat. Ces contrôles ne sont **pas un CAS** : ils ne garantissent rien contre une modification postérieure à la dernière observation du compositeur. Un succès exige le rapport final vérifié puis son acquittement durable. Après cet acquittement ou Nouvelle session, un redémarrage QuickShell dans la même session ne réaffiche pas le chooser. Un simple claim n’est pas un acquittement : après redémarrage, un état DEFERRED conserve son retry explicite, un backend encore verrouillé est observé sans relancement, et une interruption présente le diagnostic sans restauration automatique. Une intention IPC ambiguë reste `RECOVERY_REQUIRED` ; Réessayer ne contourne pas cette protection. Le gestionnaire de sessions permet sauvegarde, liste, aperçu structurel, suppression et restauration V2. L’aperçu ne modifie aucune fenêtre. Le backend fournit aussi `show` pour inspecter une session. Les noms sont limités à 64 caractères ASCII alphanumériques, tirets et underscores, commençant par un caractère alphanumérique. Ne pas publier les sessions ou rapports runtime. ## Installation reproductible Prérequis : Python 3, Sway compatible, QuickShell/Qt 6, GTK3, compilateur C17, `pkg-config`, autotiling, Kitty, Firefox et Limusic déjà configuré par l’autostart Sway. Le catalogue est `session_v2/applications.py`. Limusic utilise `strategy=managed-autostart`, `managed_by=sway-autostart` et aucun argv Labfy. Depuis la racine du dépôt : ```sh python3 -B tools/build-session-v2.py install -Dm755 build/session-v2/labfy-v2-anchor \ quickshell/.config/quickshell/labfy-sway/session_v2/libexec/labfy-v2-anchor ``` Le build ne déploie ni extension ni préférence. Le helper compilé et les archives restent ignorés par Git. Déployer le package Stow `quickshell` selon le guide habituel, puis suivre [la documentation du companion Firefox](../firefox-extension/session-v2/README.md). Pour la correction d’identité Firefox 1.1.1, les sources locales ne suffisent pas : une mise à jour XPI signée AMO unlisted puis une installation explicitement autorisée sont nécessaires. Voir le companion pour le protocole de lecture seule, l’auto-baseline uniquement après attestation native d’absence du checkpoint automatique V2 et de toute entrée de sauvegarde nommée dans `sessions` (échec fermé si contrôle inaccessible), les Ctrl+N automatiques après photographie initiale, les ambiguïtés isolées et le risque résiduel de restauration tardive indiscernable. Aucun timer ne constitue une preuve de nouveauté ni un CAS sessionstore. Les fenêtres ambiguës observées restent refusées. Ce delta n’a pas été validé au login physique et ne récupère pas un UUID déjà perdu. Versions de la recette historique du 7 octobre : Firefox 157.0, SwayFX 0.6, QuickShell 0.3.1, autotiling 1.9.4, Python 3.14.7, Qt Declarative 6.11.2 et GTK3 3.24.52. ## Architecture et invariants `sessionui/SessionV2Service.qml` est l’unique propriétaire des commandes UI. `Bar.qml` instancie `SessionStartupPromptV2` ; `shell.qml` désigne une seule barre hôte. `ControlCenter.qml` utilise `SessionManagerV2` et `SessionExitGate` avec le statut de checkpoint `saved`. Aucune surface active ne dépend du moteur V1. Le backend capture applications, slots et structure ; il reconstruit par ancres et swaps, puis vérifie arbre, absence d’ancres et focus final. Le guard possède la suspension/reprise des helpers et leur récupération en cas d’interruption. Les observations runtime ne sont pas des identités persistantes. Firefox garde la propriété de ses fenêtres/onglets ; le companion fournit l’identité exacte. Kitty restaure uniquement la fenêtre. Les échecs sont explicites ; aucune approximation d’identité Firefox n’est substituée silencieusement. Les checkpoints et sessions sont privés sous `${XDG_STATE_HOME:-$HOME/.local/state}/labfy-sway/session-v2`. Les transactions, locks, rapports et observations du provider restent sous `$XDG_RUNTIME_DIR`. Les anciennes données utilisateur V1 sont conservées. `analyze-v1-migration` est une analyse en lecture seule, sans conversion automatique ni moteur V1. Les anciens documents V1 sont des références historiques non exécutables. ## Diagnostics et validation ```sh python3 -B "$HOME/.config/quickshell/labfy-sway/session-v2.py" restore-status qs ipc -c labfy-sway call sessionV2 status systemctl --user is-active quickshell-labfy-sway.service ``` Le rapport de succès doit indiquer `status=success`, les deux vérifications finales vraies, autant d’ancres nettoyées que créées et autant de helpers repris que suspendus. Le diagnostic QuickShell doit retourner `backend=v2`, `busy=false`. Le rapport backend contient des identités privées : ne pas le joindre brut à un ticket public. Ne jamais relancer un restore pour simplement lire son statut. Tests sur compositeur headless privé, sans logout ni accès au bureau utilisateur : ```sh python3 -B quickshell/.config/quickshell/labfy-sway/tests/focus/test_focus.py python3 -B quickshell/.config/quickshell/labfy-sway/tests/focus/test_qml.py python3 -B -m unittest discover -s tests/session_v2 -p 'test_*.py' python3 -B tests/session_v2/validate_login_focus.py python3 -B tests/session_v2/runtime.py python3 -B tests/session_v2/failures.py python3 -B tests/session_v2/crashes.py python3 -B tests/session_v2/security.py python3 -B tests/session_v2/phase3b_runtime.py python3 -B tests/session_v2/phase3b_ui.py QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner \ -input tests/session_v2/qml -o -,txt ``` Les tests Python nécessitent `jsonschema` ; les scénarios UI nécessitent QuickShell et QtTest. Les résultats synthétiques restent sous `build/session-v2-tests`, ou dans `LABFY_SESSION_V2_EVIDENCE`. Les fixtures ne lancent que leur propre compositeur et leurs enfants TEST_ONLY. Le build helper C17 utilise `-Wall -Wextra -Werror`. Les métadonnées QuickShell peuvent produire des avertissements qmllint ; le chargement réel et les scénarios Qt complètent le contrôle syntaxique, sans prétendre à zéro avertissement. Validation du 17 octobre dans la copie isolée : tests backend, focus et QML, 35 scénarios runtime, erreurs contrôlées, interruptions/SIGKILL, sécurité, Limusic synthétique et scénarios QuickShell réels sur Wayland headless. Le cas `deferred-real` maintient une seconde surface QuickShell exclusive : focus Sway nul observé, refus sans journaux, aucun retry automatique, retrait explicite de la surface, activation de la fenêtre tierce par le contrôleur de test, puis demande Réessayer et succès. La fenêtre tierce retrouve son vrai focus ; Limusic synthétique reste unique. Les fonctions des boutons sont appelées par le harnais QML : ce n’est pas une recette de clic physique. Le test crash mid-command tue l’executor après mutation IPC réelle mais avant confirmation dans Recovery : l’intention durable reste pending et `RECOVERY_REQUIRED`, sans prétendre à une récupération prouvée. Les cas Limusic absente et dupliquée refusent sans lancement ni fermeture de Limusic. Le catalogue et les lignes Sway `assign [app_id="limusic-app"] workspace 10` et `exec limusic-app` restent inchangés. Dans le bac à sable Codex, `/dev/dri` était inaccessible : le SwayFX 0.6 installé n’a pas pu initialiser son renderer GLES2. Cet échec initial ne vaut pas un test fonctionnel. La première validation a donc utilisé Sway 1.12, extrait du paquet local `sway-1:1.12-4-x86_64.pkg.tar.zst` dans `/tmp`, avec Pixman, QuickShell 0.3.2 et Qt 6.12.0, sans remplacer le compositeur installé. Après réintégration contrôlée, une deuxième validation a réussi directement sur l’hôte ArchASP avec **SwayFX 0.6 installé**, en backend headless privé et avec accès au renderer de l’hôte : 35 scénarios runtime, 3 erreurs contrôlées, 10 scénarios de crash, 22 contrôles de sécurité, Limusic synthétique et 7 scénarios UI QuickShell, dont `deferred-real`. Les tests backend (27), focus (18), QML (12) et `qmllint` passent également sur les sources réintégrées. Aucun essai n’a accédé au socket Sway de la session utilisateur. **Nouvelle validation après la correction prewarm (17 octobre 2026).** Une campagne sur le SwayFX 0.6 installé, mais sur un socket Wayland/Sway IPC headless privé, a réussi les tests de focus (28), backend et prewarm (34), QML Python (13), QtTest (5) et onze scénarios composés. Le cas nominal exécute le véritable chemin de lancement Python `Popen` avec des applications GTK **synthétiques** : Limusic préexiste sur 10, Kitty et Firefox arrivent sur 1 avant toute mutation du layout ; trois swaps, trois slots, arborescence vérifiée, stage vide et focus final sur Firefox/1. Sont également vérifiés : lancement Firefox en échec puis retry sans deuxième Kitty, focus tiers préservé, mappings concurrents refusés et intentions IPC ambiguës conservées. Sur le même SwayFX headless privé, les suites runtime (35 scénarios), erreurs contrôlées (3), crash/SIGKILL (10), sécurité (22), propriété Limusic et les sept scénarios de QuickShell passent. Les sources de production validées dans la copie de test sont identiques octet pour octet à la version retenue pour cette intégration. Les tests ne lancent jamais le vrai Firefox, Kitty ou Limusic du bureau : le **prochain login réel reste non validé**, y compris l’intégration de l’extension Firefox avec son profil habituel. Voir aussi [l’investigation du premier échec](session-v2-login-focus-investigation.md). Les fixtures acceptent `LABFY_TEST_SWAY=/chemin/absolu/vers/sway` et `LABFY_TEST_RENDERER=pixman` pour une exécution sans renderer GLES2 ; pour Qt headless utiliser `QT_QPA_PLATFORM=wayland` et `QT_QUICK_BACKEND=software`. Elles imposent un backend headless, un socket privé et HOME/XDG temporaires. Le répertoire de preuves, s’il est précréé, doit avoir le mode 0700 (les JSON sont privés). Ces résultats prouvent les chemins testés sur ce compositeur headless. Ils ne prouvent ni le retour du focus avec tous les périphériques du bureau, ni le comportement du prochain login SwayFX/UWSM réel. Cette validation reste à faire séparément après réintégration contrôlée. La campagne des corrections P0, ses résultats, les refus initiaux et les commandes de reprise sur hôte sont détaillés dans [la validation prewarm privée](session-v2-prewarm-private-validation.md). Le refus DRM du SwayFX installé dans le bac à sable n’est pas un PASS SwayFX. ## Correction des chemins post-Guard du premier login Le dernier login réel fourni au diagnostic a encore refusé après intégration P0 : `TARGET_OBSERVATION_CHANGED`, zéro déplacement, zéro swap. Les journaux réels restent préservés ; ils n’ont pas été recherchés ni modifiés ici. Les chemins exacts PREWARM n’étant pas archivés, l’insertion de parents unaires par Sway/autotiling est une hypothèse forte, pas une cause historique prouvée. La contradiction démontrée est corrigée : après Guard, le lot courant déjà comparé à la phase 0 devient l’observation canonique passée aux admissions. Recovery conserve ses relectures strictes ; identité, placement, ordre et preuve de focus ne sont pas élargis. Les chemins actualisés par les commandes possédées sont acceptés après provider ; un changement externe pendant cette attente reste refusé. Les protections P0 restent en place. La campagne privée Sway 1.12/Pixman passe : 43 tests backend, suites précédentes complètes, quatre succès de quatre slots avec parents unaires (dont un avec autotiling enfant) et un refus post-provider ciblé. Le SwayFX/GLES2 du sandbox échoue au démarrage faute de DRM. **Cette correction ne valide pas le login graphique physique**, ni les applications Firefox/Kitty/Limusic réelles. Voir le [diagnostic ciblé, les preuves et la commande hôte](session-v2-path-stability-diagnostic.md). ## Limites conservées - Contenu et processus terminal non restaurés. - Ratios exacts non restaurés. - Fullscreen non pris en charge. Scratchpad pris en charge par le snapshot version 2 pour les feuilles flottantes individuelles ; les groupes restent refusés. - Multi-output physique non validé. - Provider Firefox candidat actuel pour le profil supporté ; plusieurs profils simultanés ne sont pas pris en charge. ## Snapshot version 2 — scratchpad (22 octobre 2026) Les nouvelles captures conservent `schema=labfy.sway.session-v2` et écrivent `version=2`. Le contrat V1 et `schema-v1.json` restent inchangés : les anciennes sessions sont lues, validées et restaurées directement, sans conversion ni invention d’appartenance scratchpad. `schema-v2.json` décrit la nouvelle forme ; `schema.py` ajoute les contraintes référentielles (unicité, cardinalité, chemins, focus et association workspace/output) aux contraintes locales du schéma JSON. Le bundle de développement contient les deux schémas. Dans V2, `state.scratchpad` vaut `none`, `hidden` ou `shown`. Une feuille scratchpad est nécessairement flottante, avec son rectangle IPC entier `x/y/width/height`. Une feuille cachée a `workspace=null` et `tree_path=[]` ; `__i3_scratch` n’entre jamais dans les workspaces, sorties ou ancres persistés. Une feuille montrée garde le workspace/output de visibilité. `focus` reste une référence de slot ou null, jamais une feuille déduite d’un split. Le champ V2 `focus_workspace` conserve le workspace effectivement focalisé, y compris un workspace vide lorsque toutes les fenêtres sont cachées. Ce seul workspace vide est admissible sans slot ; son output doit être disponible. Une feuille cachée ne peut être le focus persistant. La capture inclut les scratchpads cachés sous l’output interne Sway et les scratchpads montrés parmi les flottants normaux. Les relectures comparent aussi app_id, PID, appartenance scratchpad et focus ; un changement donne `SNAPSHOT_CHANGED`. Les groupes flottants/scratchpad ne sont pas aplatis en feuilles indépendantes : ils sont refusés précisément. Les fenêtres de même application sans fournisseur exact restent explicitement interchangeables. Le mapping privilégie la visibilité déjà observée pour ces slots ; une feuille scratchpad existante n’est pas exposée pour remplir arbitrairement un slot ordinaire. Les preuves UUID/singleton restent prioritaires, puis les identités runtime admises comprennent con_id/PID/starttime/session/placement. La phase de placement (`plan.placements`) est distincte de la compilation des ancres de splits. Les scratchpads existants évitent staging et `floating disable`. Les commandes sont ciblées par l’identité admise ; `scratchpad show` n’est émis que pour une feuille prouvée cachée et un slot explicitement montré. L’ajout initial au scratchpad sélectionne le workspace source, même si la cible était inactive : cette transition est prédite avant IPC. Le journal précède chaque commande. Les transitions `floating enable` et `scratchpad show` ont une attente bornée de 300 ms pour laisser passer la configuration Wayland initiale ; cette attente n’est pas une preuve de stabilité. Toute divergence encore observée reste un échec. Le rectangle intermédiaire d’un `show` peut être converti par Sway selon l’output ; le rectangle sauvegardé est ensuite commandé et vérifié exactement, avec appartenance, visibilité, output, layout, identité et focus. Les nouveaux journaux Recovery sont en version 2 et suivent l’appartenance scratchpad. Les journaux V1 restent lisibles ; une observation scratchpad absente de leurs preuves est refusée, jamais reconstruite implicitement. Après incident, un scratchpad déjà exactement à son origine peut être reconnu sans IPC. Si son placement diffère, la reprise conserve les données et donne `SCRATCHPAD_RECOVERY_REQUIRES_REVIEW` ; elle n’affiche aucune feuille pour tenter un rollback spéculatif. Les intentions pending et les conflits de focus restent bloquants. L’ancien journal utilisateur `RECOVERY_REQUIRED` n’est ni modifié ni acquitté par ce patch. Le dialogue d’échec de checkpoint montre maintenant le `reason` JSON exact, avec une explication pour les cas connus, notamment `CAPABILITY_UNSUPPORTED`. L’erreur conserve l’action en attente : annulation ou choix explicite de quitter sans sauvegarder restent nécessaires. Aucun code d’erreur n’autorise le logout. Validation et limites détaillées : [rapport scratchpad](session-v2-scratchpad-save-and-restore-v1.md).