16 KiB
Restore Plan et Executor V1 — STEP19A/STEP19B
Statut et architecture
Le planner STEP19A ne restaure jamais. L'exécution contrôlée appartient exclusivement au module séparé STEP19B décrit à la fin de ce document.
STEP19A charge un Session Snapshot V1 persistant, observe la session Sway actuelle avec les quatre lectures STEP18, puis produit un plan JSON. Il ne lance ni ne tue aucun processus, ne change aucun workspace, ne déplace aucune fenêtre et n'envoie aucune commande Sway mutatrice. L'exécuteur appartient à STEP19B ; le Spatial Canvas appartient à STEP20.
snapshot persistant validé + Session Snapshot V1 live validé
↓
session_restore.py (fonction pure)
↓
labfy.sway.restore-plan / version 1
Le point d'entrée reste :
session_snapshot.py plan <name>
session_snapshot.py possède le chargement sûr, le verrou partagé, la
validation persistante et la collecte IPC. session_restore.py ne possède ni
I/O fichier, ni subprocess, ni API Sway. Il reçoit deux documents normalisés
et réutilise donc sans duplication la classification, la résolution
DesktopEntry et l'inventaire live de STEP18.
Enveloppe Restore Plan V1
{
"schema": "labfy.sway.restore-plan",
"version": 1,
"read_only": true,
"execution_supported": false,
"source_session": "dev",
"source": {},
"live": {},
"policy": {},
"matches": [],
"unmatched_source_windows": [],
"actions": [],
"diagnostics": {}
}
Le planner n'ajoute pas d'heure courante. Les seuls timestamps viennent de ses entrées. Un même couple snapshot/live produit donc le même plan.
Chaque action contient au minimum :
action_id
action
phase
source_window
snapshot_identity
depends_on[]
depends_on exprime les chaînes futures, par exemple lancement, placement,
arbre puis focus. Il n'exécute pas ces opérations.
Inventaire live et identité runtime
live.windows[] conserve :
runtime.con_id, runtime.pid, workspace, output
app_id, class, instance, desktop_entry, executable_basename
classification.category, classification.managed_by
floating, fullscreen_mode, scratchpad, focused, title_hint
Les PID et con_id sont exposés comme observations pour un futur exécuteur
dans la session courante, mais policy.runtime_identity_fields est vide. Ils
ne participent jamais au matching persistant : ils changent, et peuvent même
être réutilisés numériquement, après logout ou reboot.
Matching one-to-one
Une arête snapshot→live n'existe que si une preuve d'identité applicative existe. Les preuves, par poids décroissant, sont :
- même DesktopEntry résolue ;
- même
app_id; - même classe XWayland/
StartupWMClass; - même instance ;
- même basename exécutable compatible.
Deux app_id explicites et différentes interdisent un match fondé seulement
sur un basename. La résolution DesktopEntry n'est pas réimplémentée : elle
provient de resolve_desktop_entry() STEP18 pour le snapshot comme pour le
live.
Le workspace identique, le même état floating et un title_hint identique
servent seulement de départage. Un titre ne crée jamais un candidat et n'est
jamais une identité obligatoire.
Le planner trie globalement toutes les arêtes par preuve, puis applique un matching one-to-one. Une fenêtre live ne peut satisfaire qu'une fenêtre sauvegardée. Pour deux Firefox sauvegardées et une live, une seule est réutilisée ; l'autre reste absente. L'ordre snapshot ne sert qu'au dernier départage.
Confiance
exact DesktopEntry identique + app_id ou classe identique
high DesktopEntry, app_id ou classe exacte sans preuve combinée
medium instance ou basename compatible
ambiguous plusieurs arêtes de meilleur score subsistent
unresolved aucune preuve suffisante
exact n'est jamais déduit du PID, du con_id, du titre seul ou de la seule
position dans l'arbre.
Classification et lancement futur
Applications utilisateur
Une application absente n'est proposée au lancement que si sa DesktopEntry a
été résolue avec confiance STEP18 exact. Une identité heuristique ou
absente produit manual-required ; aucune ligne de commande n'est inventée.
Le futur backend STEP19B est figé conceptuellement comme :
{
"backend": "uwsm-app-desktop-entry",
"argv": ["uwsm", "app", "--", "firefox.desktop"],
"execute_in_step19a": false
}
L'audit de uwsm 0.27.0 confirme que uwsm app accepte un exécutable, un
identifiant DesktopEntry ou son chemin, et lance l'application comme scope ou
service dans une slice graphique. Sa documentation recommande ce mécanisme
pour ne pas accumuler les applications dans l'unité du compositeur. STEP19B
devra donc préférer l'identifiant DesktopEntry via uwsm app à gtk-launch,
gio launch ou un enfant direct de Sway. STEP19A ne l'exécute jamais.
Le lancement ne promet pas de restaurer l'état interne d'un terminal, les onglets d'un navigateur ou le contenu propre à une application.
Applications gérées par autostart
Une vue présente est réutilisée normalement. Une vue absente produit
skip-autostart-managed, jamais launch-application, car sway-autostart
possède son lancement. C'est notamment le contrat de limusic-app issu de
exec limusic-app. STEP19B devra attendre ou demander une intervention au
lieu de créer une double instance.
Infrastructure et daemons
session-infrastructure et daemon ne produisent jamais de lancement.
QuickShell, Polkit, autotiling, wlsunset, les services systemd utilisateur et
le listener de transparence restent possédés par leur sous-système. Une
absence inattendue produit manual-required.
Workspaces et outputs
Chaque action conserve desired_workspace et desired_output. Une fenêtre
live au mauvais endroit produit reuse-window, puis move-to-workspace, sans
lancement supplémentaire.
Les outputs sont appariés par nom de connecteur. La politique V1 est :
connector name preferred
missing output = deferred-no-fallback
Si le connecteur sauvegardé est absent, le planner produit
manual-required(reason=missing-output) et n'invente ni écran ni fallback. Le
lancement d'une application absente est lui aussi différé dans ce cas.
Arbre Sway natif
Le planner compare une signature sémantique par vue : layout/orientation du
workspace, chaîne des containers parents, layout/orientation/percent des
parents, branche, index et percent de la fenêtre. Les container_id runtime
sont exclus.
Une différence produit restore-tree-position avec
capability=partially-supported. Sway natif sait déplacer, splitter, choisir
un layout et ajuster des proportions, mais V1 ne prétend pas reconstruire
atomiquement tout arbre arbitraire. STEP19B devra valider ses primitives et
replanifier après l'apparition des fenêtres.
Seul layout_engine=sway-native est accepté. spatial-canvas-v1 reste hors
périmètre jusqu'à STEP20.
Floating, fullscreen et scratchpad
Une différence live/source produit une intention distincte :
restore-floating
restore-fullscreen
restore-scratchpad-hidden
restore-scratchpad-visible
Une fenêtre live présente dans le scratchpad alors que le snapshot ne le
demande pas produit manual-required, plutôt qu'une hypothèse destructrice.
Ordre et focus
Le focus est toujours la dernière phase. Il n'est ajouté que si le focus live diffère ou si une intention antérieure pourrait le perturber.
1. match-managed : réutiliser/différer/bloquer
2. launch : intentions de lancement exactes
3. wait-match : réservé au rematching STEP19B
4. workspace : placement workspace/output
5. tree-layout : arbre Sway natif
6. floating
7. fullscreen
8. scratchpad
9. focus
Le validateur refuse une dépendance future, un recul de phase ou une action après le focus.
Actions V1 et refus explicites
reuse-window
launch-application
skip-autostart-managed
move-to-workspace
restore-tree-position
restore-floating
restore-fullscreen
restore-scratchpad-hidden
restore-scratchpad-visible
restore-focus
manual-required
manual-required porte notamment les raisons suivantes :
no-exact-launch-identity
missing-output
non-restorable-category
live-window-unexpected-scratchpad
Dire que l'automatisation ne sait pas est préférable à une commande inventée.
Garanties et limites STEP19A
Le module planner n'importe pas subprocess, ne contient pas swaymsg, et ne
possède aucune fonction de lancement, d'écriture ou de suppression. Le CLI
n'appelle que :
show_session(name) # lecture et validation persistante
collect(autostart) # quatre IPC read-only STEP18
build_restore_plan() # fonction pure
Le plan expose :
read_only = true
execution_supported = false
mutating_commands_executed = 0
applications_launched = 0
snapshots_written = 0
Le planner n'effectue aucune attente de fenêtre, aucune commande Sway mutatrice, aucune restauration d'état interne, aucun fallback multi-output, aucune UI QML et aucun comportement Spatial Canvas.
Executor V1 — STEP19B
Frontière d'effets et CLI
session_restore.py reste le moteur pur. session_executor.py est la seule
frontière autorisée à ouvrir le lock d'exécution et à lancer des argv directs.
Session Snapshot V1 validé
↓
Restore Planner pur
↓
Restore Plan V1 frais
↓
Session Executor V1
↓
uwsm app + Sway IPC ciblé
La mutation nécessite une intention explicite difficile à déclencher par accident :
session_snapshot.py apply <name> --execute
apply <name> sans --execute retourne
RESTORE_EXECUTION_REQUIRES_EXPLICIT_EXECUTE sans charger l'exécuteur. Le CLI
n'accepte jamais un fichier Restore Plan comme autorité. Chaque apply exécute
obligatoirement :
load snapshot → collect live → build fresh plan → validate → preflight → execute
Le snapshot est hashé avant et après l'opération. Une différence produit un échec explicite ; l'exécuteur ne possède aucune API d'écriture de snapshot.
Capability gate intégrale
Avant le premier lancement ou la première commande Sway, l'exécuteur examine toutes les actions. Le sous-ensemble STEP19B est :
reuse-window no-op revalidé
launch-application uwsm app + attente/rematching
skip-autostart-managed no-op explicite, propriétaire inchangé
move-to-workspace workspace numérique seulement
restore-floating enable/disable déterministe
restore-fullscreen modes 0 et 1 seulement
restore-focus toujours dernière mutation
Ces actions bloquent l'ensemble du plan avec
RESTORE_BLOCKED_UNSUPPORTED_ACTION et une liste précise, sans aucune mutation :
restore-tree-position
restore-scratchpad-hidden
restore-scratchpad-visible
manual-required
toute action inconnue
Un matching runtime ambiguous, un workspace non numérique et un mode
fullscreen non prouvé bloquent eux aussi le plan. Une fenêtre absente n'a pas
encore de position d'arbre observable ; le planner n'émet donc une différence
restore-tree-position que pour une fenêtre déjà matchée. STEP19C pourra
replanifier l'arbre après apparition.
Lock privé
Un apply détient un flock(LOCK_EX|LOCK_NB) sur .restore.lock pendant toute
la séquence load/plan/execute/rehash. Le fichier est régulier, ouvert sans
suivre les symlinks et forcé à 0600 dans le répertoire sessions 0700. Le
fichier peut persister ; la fermeture du FD libère le lock. Aucun daemon ou
scheduler supplémentaire n'est créé.
DesktopEntry et lancement UWSM
launch.argv dans le plan reste purement descriptif et n'est jamais exécuté.
Pour chaque lancement, l'exécuteur vérifie :
action = launch-application
backend = uwsm-app-desktop-entry
classification = user-application
restore_identity.confidence = exact
DesktopEntry ID syntaxiquement bornée
résolution exacte identique dans les sources XDG courantes
Il reconstruit ensuite lui-même exactement :
["uwsm", "app", "--", desktop_entry]
Il n'utilise ni shell, ni Sway exec, ni gtk-launch, ni gio launch. Comme
uwsm app peut rester attaché pendant toute la vie de l'application, le
processus est lancé avec un argv direct, sorties vers DEVNULL et une nouvelle
session de processus. Son PID et son exit status ne prouvent jamais le succès.
Apparition et matching post-launch
L'exécuteur conserve l'inventaire des con_id avant lancement, puis poll
get_tree via la collecte Session Snapshot V1 pendant au plus 10 secondes.
Seules les nouvelles fenêtres sont candidates. Il réutilise directement
session_restore.match_windows() ; il ne possède aucun second matcher.
Zéro candidate compatible conduit à RESTORE_LAUNCH_TIMEOUT. Plusieurs
candidates indiscernables conduisent à RESTORE_LAUNCH_AMBIGUOUS. Le PID du
processus lancé n'intervient pas, ce qui couvre aussi une application qui
demanderait à une instance existante de créer une nouvelle fenêtre.
Revalidation runtime et commandes ciblées
Avant chaque mutation, une nouvelle collecte doit retrouver le même con_id,
une identité application compatible et un workspace connu. Cela interdit de
cibler silencieusement un con_id disparu ou recyclé. Après la commande, une
attente bornée à une seconde relit l'arbre jusqu'à observer la postcondition,
en revalidant l'identité à chaque lecture. Elle absorbe uniquement le délai de
visibilité IPC ; son expiration reste un échec critique.
Les workspaces exécutables sont des chaînes numériques canoniques positives. Les noms libres restent hors périmètre afin qu'aucun quoting Sway non prouvé ne devienne une surface d'injection. Un déplacement reconstruit uniquement :
[con_id=<entier validé>] move container to workspace number <entier validé>
Floating utilise exclusivement floating enable ou floating disable.
L'audit de sway(5) 1.12.0 prouve fullscreen enable|disable [global] : V1
supporte fullscreen_mode=0 avec fullscreen disable et le mode workspace
fullscreen_mode=1 avec fullscreen enable. Le mode global et toute autre
valeur sont bloqués. Aucun état ne recourt à toggle.
Le focus utilise [con_id=<id>] focus, après la même revalidation, et reste la
dernière mutation. La disparition de la cible ne focalise jamais une autre
fenêtre par défaut.
Rapport, arrêt et absence de rollback magique
Chaque action du rapport possède pending, success, skipped, failed ou
blocked, avec un reason. L'exécution s'arrête à la première divergence
critique et laisse les actions suivantes pending.
Les lancements et mutations Sway ne sont pas transactionnels. Executor V1 ne promet aucun rollback global : sa politique est un preflight total strict, des commandes ciblées, une vérification après chaque commande, puis un arrêt immédiat au premier écart.
Validation contrôlée STEP19B
La validation réelle utilise exclusivement une Kitty créée pour le test sur
un workspace numérique libre. Le scénario A réutilise la fenêtre existante et
la replace par con_id, sans nouveau lancement. Le scénario B ferme seulement
cette fenêtre, lance kitty.desktop via UWSM, observe une nouvelle fenêtre,
la rematche et la replace. Le cleanup ferme uniquement le nouveau con_id,
supprime uniquement le snapshot de validation et restaure le focus utilisateur
initial lorsqu'il existe encore.
Les fenêtres non marquées TEST_ONLY sont comparées avant/après sur
con_id, workspace, floating et fullscreen. L'arbre complet, le scratchpad
avancé, le Spatial Canvas et toute UI QML restent hors STEP19B.