Dotfiles_ArchAP/docs/session-restore-v2.md

340 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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