24 KiB
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 :
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.
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
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 :
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.
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. 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.
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.
Précondition Secret Service avant restauration — 22 octobre 2026
Le parcours utilisateur ajoute une barrière D-Bus indépendante du checkpoint.
Les CLI apply-last et apply exigent la collection default attendue réellement
déverrouillée ; le chooser propose son déverrouillage officiel et conserve le
clic explicite Restaurer après READY. Nouvelle session reste disponible sans
trousseau. Cette précondition ne modifie aucune postcondition de l'Executor,
aucun schéma de checkpoint/Recovery et ne résout pas automatiquement un
RECOVERY_REQUIRED. Une activation tardive garde les refus de focus existants.
Implémentation, audit PAM (aucun patch applicable établi), tests isolés, installation et rollback : trousseau avant restauration.
ChatGPT XWayland et WhatsApp Web (27 octobre 2026)
Le catalogue ajoute deux applications précises à Firefox, Kitty et Limusic :
| Application | Identité observée | Politique |
|---|---|---|
| ChatGPT | Fenêtre X11 avec WM_CLASS exactement Chatgpt |
single-window, lancement /usr/bin/chatgpt si absent |
| WhatsApp Web | firefox.webapp-fcd72ebe-94c5-4209-b3f6-bbaa7784cded |
managed-autostart, propriétaire sway-autostart |
L'identité X11 est une clé runtime réservée x11-class:Chatgpt. Elle exige un
identifiant de fenêtre X11 et une classe exacte ; un app_id Wayland ne peut pas
usurper ce préfixe. Les arbres IPC restent bruts. La classification, les
observations de récupération, le préchauffage, les swaps et les contrôles finaux
emploient la même clé, toujours avec con_id, PID et starttime. Un ancien journal
X11 sans classe ne reçoit pas rétroactivement une identité prouvée.
La capture refuse plusieurs fenêtres ChatGPT avec cette politique singleton. Les applications absentes du catalogue restent refusées explicitement ; elles ne sont pas retirées silencieusement de la sauvegarde. Le snapshot conserve son format version 2 et ne contient ni commande, ni chemin de document, ni contenu de conversation.
WhatsApp est assignée au workspace 9. La ligne d’autostart corrigée par
l’utilisateur lance directement Firefox avec -taskbar-tab, -new-window et
-container 0, sans intermédiaire bash -c et sans -profile.
La restauration attend cette fenêtre et ne lance pas une deuxième instance.
Le provider Firefox reste responsable des fenêtres du navigateur ordinaire.
Les tests ciblés XWayland sont dans tests/session_v2/test_xwayland.py et les
tests existants de préchauffage/autostart restent applicables. Le scénario
tests/session_v2/xwayland_runtime.py restaure deux surfaces synthétiques dans
un Sway privé : Chatgpt via XWayland et WhatsApp via Wayland. Il vérifie aussi
la préservation stricte d’une fenêtre X11 inconnue hors du lot. La capture du
bureau réel a également été contrôlée avec ChatGPT puis avec WhatsApp ouverte.
Une capture réussie ne vaut pas validation d'une déconnexion/reconnexion physique.
Un incident antérieur RECOVERY_REQUIRED continue de demander le traitement
nominatif décrit dans session-v2-focus-recovery-fix-v1.md, indépendamment de
la création d'un nouveau checkpoint.