From a9e324b156f1b4f25e18561f85cdd6eb0a4185bd Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:02:42 +0200 Subject: [PATCH] Harden session recovery, input handling, and persistence --- .../src/app/pages/session/catalog/tests.rs | 1 + configurator/src/app/search/tests.rs | 1 + configurator/src/app/session_catalog/tests.rs | 1 + .../src/app/update/session_catalog/tests.rs | 1 + configurator/src/models/session.rs | 31 +- configurator/src/models/session/tests.rs | 30 + docs/codebase-overview.md | 49 +- .../backend/event_loop/session_save.rs | 360 +------ .../event_loop/session_save/diagnostics.rs | 66 ++ .../event_loop/session_save/final_save.rs | 362 +++++++ .../event_loop/session_save/interaction.rs | 85 ++ .../event_loop/session_save/notifications.rs | 104 +- .../backend/event_loop/session_save/tests.rs | 123 ++- src/backend/wayland/handlers/AGENTS.md | 1 + src/backend/wayland/handlers/mod.rs | 3 + src/backend/wayland/handlers/pointer/axis.rs | 9 +- .../wayland/handlers/pointer/enter_leave.rs | 10 +- src/backend/wayland/handlers/pointer/mod.rs | 5 +- .../wayland/handlers/pointer/motion.rs | 23 +- src/backend/wayland/handlers/pointer/press.rs | 221 +--- .../wayland/handlers/pointer/press/tests.rs | 17 + .../wayland/handlers/pointer/press/toolbar.rs | 162 +++ .../wayland/handlers/pointer/release.rs | 40 + src/backend/wayland/handlers/pointer/tests.rs | 251 +++++ src/backend/wayland/handlers/tablet/frame.rs | 261 ++--- .../wayland/handlers/tablet/frame/tests.rs | 390 +++++++ src/backend/wayland/handlers/tablet/tool.rs | 240 ++--- .../wayland/handlers/tablet/tool/tests.rs | 96 ++ src/backend/wayland/handlers/test_support.rs | 172 +++ src/backend/wayland/handlers/touch.rs | 2 +- src/backend/wayland/session.rs | 119 +-- src/backend/wayland/session/AGENTS.md | 1 + src/backend/wayland/session/driver.rs | 208 +++- src/backend/wayland/session/driver/tests.rs | 47 +- .../driver/tests/lifecycle_regressions.rs | 3 +- .../wayland/session/driver/tests/output.rs | 978 ++++++++++++++++++ src/backend/wayland/session/home.rs | 22 - src/backend/wayland/session/home/tests.rs | 324 +++++- src/backend/wayland/session/load_outcome.rs | 82 +- .../wayland/session/persistence/worker.rs | 31 +- src/backend/wayland/session/runtime.rs | 8 +- .../wayland/session/runtime/transaction.rs | 105 +- .../session/runtime/transaction/output.rs | 215 ++++ .../session/runtime/transaction/phases.rs | 6 +- src/backend/wayland/session/tests.rs | 84 +- src/backend/wayland/session/write_policy.rs | 87 ++ src/backend/wayland/state.rs | 8 +- src/backend/wayland/state/AGENTS.md | 2 + src/backend/wayland/state/clipboard.rs | 100 +- .../wayland/state/clipboard/probe_tests.rs | 170 +++ src/backend/wayland/state/contact_owner.rs | 142 +++ src/backend/wayland/state/core/accessors.rs | 2 +- src/backend/wayland/state/core/output.rs | 11 +- .../wayland/state/core/output/session_ops.rs | 142 +-- .../wayland/state/core/output/tests.rs | 97 +- .../wayland/state/core/output/transition.rs | 190 ++-- src/backend/wayland/state/core/session.rs | 28 +- src/backend/wayland/state/input_actions.rs | 2 +- src/backend/wayland/state/onboarding/card.rs | 12 +- .../wayland/state/onboarding/first_run.rs | 2 +- src/backend/wayland/state/pointer_runtime.rs | 424 +------- .../wayland/state/pointer_runtime/chrome.rs | 73 ++ .../wayland/state/pointer_runtime/contacts.rs | 307 ++++++ .../wayland/state/pointer_runtime/tests.rs | 227 ++++ .../wayland/state/pointer_runtime/touch.rs | 53 + src/backend/wayland/state/tablet_runtime.rs | 236 ++++- .../wayland/state/tablet_runtime/pressure.rs | 135 +++ .../wayland/state/toolbar/drag/tests.rs | 36 + src/backend/wayland/state/toolbar/events.rs | 5 +- .../wayland/state/toolbar/events/session.rs | 120 +-- .../state/toolbar/events/session/queued.rs | 67 ++ .../state/toolbar/events/session/tests.rs | 98 ++ src/backend/wayland/state/toolbar/fade.rs | 5 +- .../wayland/state/toolbar/inline/input.rs | 39 + src/canvas_export/tests/board_grid.rs | 5 +- src/canvas_export/tests/cache_tests.rs | 5 +- src/config/AGENTS.md | 1 + src/config/io.rs | 2 +- src/config/tests/document.rs | 22 +- src/config/tests/file_io.rs | 6 +- src/config/tests/load.rs | 26 +- src/config/tests/validate.rs | 2 +- src/config/tests/write_lock.rs | 2 +- src/config/tests/write_target.rs | 2 +- src/env_vars.rs | 2 - .../state/core/board_picker/appearance.rs | 17 +- .../core/board_picker/appearance/tests.rs | 24 +- .../state/core/board_picker/layout/helpers.rs | 33 - src/input/state/core/board_picker/mod.rs | 32 - .../state/core/board_picker/state/ordering.rs | 4 - src/input/state/core/mod.rs | 2 + src/input/state/core/selection/clipboard.rs | 71 -- .../state/core/selection_actions/clipboard.rs | 89 +- .../selection_actions/clipboard/duplicate.rs | 1 - src/input/state/core/session.rs | 10 - src/input/state/core/test_support.rs | 71 ++ src/input/state/core/utility/pending.rs | 84 +- .../state/interaction/adapters/pointer.rs | 5 + src/input/state/interaction/mod.rs | 1 + src/input/state/interaction/outcome.rs | 40 + src/input/state/mouse/press.rs | 5 +- src/input/state/tests/selection/duplicate.rs | 257 +---- src/onboarding.rs | 2 +- src/palette_recents.rs | 2 +- src/session/AGENTS.md | 5 + src/session/artifacts.rs | 32 +- src/session/artifacts/corrupt_copies.rs | 104 ++ src/session/artifacts/kind.rs | 49 + src/session/artifacts/move_file.rs | 31 +- src/session/artifacts/tests.rs | 92 ++ src/session/mod.rs | 21 +- src/session/options/types.rs | 20 - src/session/primary.rs | 35 + src/session/snapshot/compat_tests.rs | 467 +++++++++ .../compat/main-a2bb723a/backup-lines-1.json | 38 + .../main-a2bb723a/backup-recoverable.marker | 1 + .../fixtures/compat/main-a2bb723a/blank.json | 29 + .../compat/main-a2bb723a/cleared.marker | 1 + .../compat/main-a2bb723a/lines-1.json | 38 + .../compat/main-a2bb723a/lines-2.json | 58 ++ .../compat/main-a2bb723a/lines-4.json.gz | Bin 0 -> 335 bytes .../main-a2bb723a/recovery-lines-3.json | 78 ++ .../main-a2bb723a/recovery-recoverable.marker | 1 + .../compat/v0.9.25/backup-lines-1.json | 38 + .../compat/v0.9.25/backup-recoverable.marker | 1 + .../fixtures/compat/v0.9.25/blank.json | 29 + .../fixtures/compat/v0.9.25/cleared.marker | 1 + .../fixtures/compat/v0.9.25/lines-1.json | 38 + .../fixtures/compat/v0.9.25/lines-2.json | 58 ++ .../fixtures/compat/v0.9.25/lines-4.json.gz | Bin 0 -> 335 bytes .../compat/v0.9.25/recovery-lines-3.json | 78 ++ .../v0.9.25/recovery-recoverable.marker | 1 + src/session/snapshot/generation.rs | 96 ++ src/session/snapshot/generation/marker.rs | 68 ++ src/session/snapshot/generation/probe.rs | 170 +++ src/session/snapshot/generation/tests.rs | 626 +++++++++++ src/session/snapshot/generation/view.rs | 167 +++ src/session/snapshot/load.rs | 160 +-- src/session/snapshot/load/artifact_outcome.rs | 129 +++ src/session/snapshot/load/corrupt.rs | 382 ++++++- src/session/snapshot/load/fallback.rs | 96 +- src/session/snapshot/load/markers.rs | 32 +- src/session/snapshot/load/named_candidate.rs | 22 +- src/session/snapshot/mod.rs | 12 +- src/session/snapshot/save.rs | 147 +-- src/session/snapshot/save/estimate.rs | 67 ++ src/session/snapshot/save/model.rs | 14 +- src/session/snapshot/save/payload.rs | 57 +- src/session/snapshot/save/recovery.rs | 107 +- src/session/snapshot/save/save_as.rs | 8 +- src/session/snapshot/tests.rs | 202 +++- src/session/snapshot/types.rs | 16 + src/session/storage/clear.rs | 79 +- src/session/storage/tests.rs | 30 + src/session/storage/tool_state.rs | 21 +- src/session/tests/limits.rs | 16 + src/session/tests/roundtrip.rs | 6 +- .../repository_guards/shared_dependencies.rs | 87 +- .../shared_dependency_fixtures.json | 36 + 159 files changed, 10322 insertions(+), 2870 deletions(-) create mode 100644 src/backend/wayland/backend/event_loop/session_save/diagnostics.rs create mode 100644 src/backend/wayland/backend/event_loop/session_save/final_save.rs create mode 100644 src/backend/wayland/backend/event_loop/session_save/interaction.rs create mode 100644 src/backend/wayland/handlers/pointer/press/tests.rs create mode 100644 src/backend/wayland/handlers/pointer/press/toolbar.rs create mode 100644 src/backend/wayland/handlers/pointer/tests.rs create mode 100644 src/backend/wayland/handlers/tablet/frame/tests.rs create mode 100644 src/backend/wayland/handlers/tablet/tool/tests.rs create mode 100644 src/backend/wayland/handlers/test_support.rs create mode 100644 src/backend/wayland/session/driver/tests/output.rs create mode 100644 src/backend/wayland/session/runtime/transaction/output.rs create mode 100644 src/backend/wayland/session/write_policy.rs create mode 100644 src/backend/wayland/state/clipboard/probe_tests.rs create mode 100644 src/backend/wayland/state/contact_owner.rs create mode 100644 src/backend/wayland/state/pointer_runtime/chrome.rs create mode 100644 src/backend/wayland/state/pointer_runtime/contacts.rs create mode 100644 src/backend/wayland/state/pointer_runtime/tests.rs create mode 100644 src/backend/wayland/state/pointer_runtime/touch.rs create mode 100644 src/backend/wayland/state/tablet_runtime/pressure.rs create mode 100644 src/backend/wayland/state/toolbar/events/session/queued.rs create mode 100644 src/backend/wayland/state/toolbar/events/session/tests.rs create mode 100644 src/input/state/core/test_support.rs create mode 100644 src/session/artifacts/corrupt_copies.rs create mode 100644 src/session/artifacts/kind.rs create mode 100644 src/session/snapshot/compat_tests.rs create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/backup-lines-1.json create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/backup-recoverable.marker create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/blank.json create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/cleared.marker create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/lines-1.json create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/lines-2.json create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/lines-4.json.gz create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/recovery-lines-3.json create mode 100644 src/session/snapshot/fixtures/compat/main-a2bb723a/recovery-recoverable.marker create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/backup-lines-1.json create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/backup-recoverable.marker create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/blank.json create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/cleared.marker create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/lines-1.json create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/lines-2.json create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/lines-4.json.gz create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/recovery-lines-3.json create mode 100644 src/session/snapshot/fixtures/compat/v0.9.25/recovery-recoverable.marker create mode 100644 src/session/snapshot/generation.rs create mode 100644 src/session/snapshot/generation/marker.rs create mode 100644 src/session/snapshot/generation/probe.rs create mode 100644 src/session/snapshot/generation/tests.rs create mode 100644 src/session/snapshot/generation/view.rs create mode 100644 src/session/snapshot/load/artifact_outcome.rs create mode 100644 src/session/snapshot/save/estimate.rs diff --git a/configurator/src/app/pages/session/catalog/tests.rs b/configurator/src/app/pages/session/catalog/tests.rs index ca61b9129..2a7f42511 100644 --- a/configurator/src/app/pages/session/catalog/tests.rs +++ b/configurator/src/app/pages/session/catalog/tests.rs @@ -18,6 +18,7 @@ fn test_item(id: &str, display_name: &str) -> SessionCatalogItem { primary_exists: true, backup_exists: false, recovery_exists: false, + unreadable_copy_exists: false, clear_marker_exists: false, lock_exists: false, non_lock_size_bytes: 2048, diff --git a/configurator/src/app/search/tests.rs b/configurator/src/app/search/tests.rs index c985c342d..02ddd6b33 100644 --- a/configurator/src/app/search/tests.rs +++ b/configurator/src/app/search/tests.rs @@ -842,6 +842,7 @@ fn catalog_item(id: &str) -> SessionCatalogItem { primary_exists: false, backup_exists: false, recovery_exists: false, + unreadable_copy_exists: false, clear_marker_exists: false, lock_exists: false, non_lock_size_bytes: 0, diff --git a/configurator/src/app/session_catalog/tests.rs b/configurator/src/app/session_catalog/tests.rs index ee903c0d9..ff692fbcb 100644 --- a/configurator/src/app/session_catalog/tests.rs +++ b/configurator/src/app/session_catalog/tests.rs @@ -194,6 +194,7 @@ fn session_artifact_status_reports_size_when_present() { primary_exists: true, backup_exists: false, recovery_exists: false, + unreadable_copy_exists: false, clear_marker_exists: false, lock_exists: false, non_lock_size_bytes: 4096, diff --git a/configurator/src/app/update/session_catalog/tests.rs b/configurator/src/app/update/session_catalog/tests.rs index d87b7cae2..ffc43df96 100644 --- a/configurator/src/app/update/session_catalog/tests.rs +++ b/configurator/src/app/update/session_catalog/tests.rs @@ -21,6 +21,7 @@ fn catalog_item(id: &str, display_name: &str) -> SessionCatalogItem { primary_exists: false, backup_exists: false, recovery_exists: false, + unreadable_copy_exists: false, clear_marker_exists: false, lock_exists: false, non_lock_size_bytes: 0, diff --git a/configurator/src/models/session.rs b/configurator/src/models/session.rs index 5627e3cbb..e2e9881c6 100644 --- a/configurator/src/models/session.rs +++ b/configurator/src/models/session.rs @@ -242,6 +242,7 @@ pub struct SessionArtifactSummary { pub primary_exists: bool, pub backup_exists: bool, pub recovery_exists: bool, + pub unreadable_copy_exists: bool, pub clear_marker_exists: bool, pub lock_exists: bool, pub non_lock_size_bytes: u64, @@ -252,34 +253,35 @@ impl SessionArtifactSummary { let artifacts = wayscriber::session::named_session_artifact_paths(path); let non_lock_paths = wayscriber::session::named_session_non_lock_artifact_paths(path) .map_err(|err| err.to_string())?; - let recovery_name = artifacts - .recovery - .file_name() - .and_then(|name| name.to_str()) - .map(str::to_string); let mut summary = Self { primary_exists: artifact_exists(&artifacts.primary)?, backup_exists: artifact_exists(&artifacts.backup)? || artifact_exists(&artifacts.backup_recovery_marker)?, recovery_exists: false, + unreadable_copy_exists: false, clear_marker_exists: artifact_exists(&artifacts.clear_marker)?, lock_exists: artifact_exists(&artifacts.lock)?, non_lock_size_bytes: 0, }; - for path in non_lock_paths { - let Some(metadata) = artifact_metadata(&path)? else { + for artifact in non_lock_paths { + let Some(metadata) = artifact_metadata(&artifact)? else { continue; }; summary.non_lock_size_bytes = summary.non_lock_size_bytes.saturating_add(metadata.len()); - if recovery_name.as_deref().is_some_and(|name| { - path.file_name() - .and_then(|value| value.to_str()) - .is_some_and(|value| value == name || value.starts_with(&format!("{name}."))) - }) { - summary.recovery_exists = true; + match wayscriber::session::named_session_artifact_kind(path, &artifact) { + Some(wayscriber::session::SessionArtifactKind::Backup) => { + summary.backup_exists = true + } + Some(wayscriber::session::SessionArtifactKind::Recovery) => { + summary.recovery_exists = true + } + Some(wayscriber::session::SessionArtifactKind::UnreadableCopy) => { + summary.unreadable_copy_exists = true + } + _ => {} } } @@ -297,6 +299,9 @@ impl SessionArtifactSummary { if self.recovery_exists { parts.push("recovery"); } + if self.unreadable_copy_exists { + parts.push("unreadable copy"); + } if self.clear_marker_exists { parts.push("cleared"); } diff --git a/configurator/src/models/session/tests.rs b/configurator/src/models/session/tests.rs index 6b9ebd578..e3d8f65f9 100644 --- a/configurator/src/models/session/tests.rs +++ b/configurator/src/models/session/tests.rs @@ -16,6 +16,7 @@ fn session_catalog_state_replaces_items_and_inputs() { primary_exists: false, backup_exists: false, recovery_exists: false, + unreadable_copy_exists: false, clear_marker_exists: false, lock_exists: false, non_lock_size_bytes: 0, @@ -90,3 +91,32 @@ fn format_byte_count_uses_compact_units() { assert_eq!(format_byte_count(4096), "4.0 KiB"); assert_eq!(format_byte_count(2 * 1024 * 1024), "2.0 MiB"); } + +#[test] +fn artifact_summary_counts_diagnostic_copies_without_counting_similar_foreign_files() { + let temp = crate::test_temp::tempdir().unwrap(); + let primary = temp.path().join("board.wayscriber-session"); + std::fs::write(&primary, b"primary").unwrap(); + for suffix in [".corrupt-1", ".bak.corrupt-2", ".recovery.corrupt-3"] { + std::fs::write(format!("{}{suffix}", primary.display()), b"diagnostic").unwrap(); + } + std::fs::write( + format!("{}.corrupt-01", primary.display()), + b"foreign similar name", + ) + .unwrap(); + let summary = SessionArtifactSummary::from_primary_path(&primary).unwrap(); + assert!(summary.primary_exists); + // Unreadable copies restore nothing, so they are not shown as a backup + // or recovery file. + assert!(!summary.backup_exists); + assert!(!summary.recovery_exists); + assert!(summary.unreadable_copy_exists); + assert_eq!(summary.non_lock_size_bytes, 7 + 3 * 10); + assert_eq!(summary.status_label(), "primary, unreadable copy · 37 B"); + + // A primary moved aside still leaves something to show. + std::fs::remove_file(&primary).unwrap(); + let summary = SessionArtifactSummary::from_primary_path(&primary).unwrap(); + assert_eq!(summary.status_label(), "unreadable copy · 30 B"); +} diff --git a/docs/codebase-overview.md b/docs/codebase-overview.md index ae0174753..497098e01 100644 --- a/docs/codebase-overview.md +++ b/docs/codebase-overview.md @@ -77,7 +77,7 @@ Daemon mode therefore provides a persistent background service that reacts to us - Communicate with `capture::CaptureManager` for screenshot actions. - Exit when `InputState.should_exit` is set (Escape, tray close, etc.). -`WaylandState` coordinates the runtime owners handlers need. `FocusState` owns activation, focus, and startup acquisition; `ProtocolGlobals` owns bound globals and toolkit handler state; `PointerRuntime` owns pointer position, board-pan and chrome gestures, cursor, pointer-lock, and single-contact touch protocol lifecycles; `ToolbarChrome` owns toolbar placement, inline interaction, and fade state; `ToolbarDrag` owns built-in and GTK drag lifecycles; `RegionCaptureRuntime` owns region selection generations, active/review/window-snap state, and the window-query and cut-preview workers; `AcquisitionRuntime` owns the capacity-one screen-acquisition and zoom-waiter registries plus eyedropper source correlation; `FrozenState` owns its availability and one-shot startup gate; `SurfaceState` owns output/fullscreen/layer placement and frozen-fullscreen transitions; `OverlaySuppressionState` owns suppression reason, keyboard policy, capture barrier, and clickthrough state; `RenderRuntime` owns the canvas layer cache, reusable image and blur caches, resolved theme, UI paint caches, render-profile baseline, and per-effect damage history; `InputHudRuntime` owns system-reader lifecycle and reconciliation; `SpotlightRuntime` owns render memory, warning latches, and wheel timing; `ClipboardRuntime` owns single-flight clipboard workers and queue policy; `PreferenceStores` groups durable preference stores and workers; `UiAnimationClock` owns animation scheduling; and `FontCatalogPrewarm` owns the one-shot font scan. The root retains cross-owner coordination. `handlers::route::SurfaceRouter` is the single classifier for pointer, touch, and stylus surfaces and supplies overlay screen coordinates before modality-specific dispatch. +`WaylandState` coordinates the runtime owners handlers need. `FocusState` owns activation, focus, and startup acquisition; `ProtocolGlobals` owns bound globals and toolkit handler state; `PointerRuntime` owns pointer position, board-pan and chrome gestures, cursor, pointer-lock, and single-contact touch protocol lifecycles; `ToolbarChrome` owns toolbar placement, inline interaction, and fade state; `ToolbarDrag` owns built-in and GTK drag lifecycles; `RegionCaptureRuntime` owns region selection generations, active/review/window-snap state, and the window-query and cut-preview workers; `AcquisitionRuntime` owns the capacity-one screen-acquisition and zoom-waiter registries plus eyedropper source correlation; `FrozenState` owns its availability and one-shot startup gate; `SurfaceState` owns output/fullscreen/layer placement and frozen-fullscreen transitions; `OverlaySuppressionState` owns suppression reason, keyboard policy, capture barrier, and clickthrough state; `RenderRuntime` owns the canvas layer cache, reusable image and blur caches, resolved theme, UI paint caches, render-profile baseline, and per-effect damage history; `InputHudRuntime` owns system-reader lifecycle and reconciliation; `SpotlightRuntime` owns render memory, warning latches, and wheel timing; `ClipboardRuntime` owns single-flight clipboard workers and queue policy; `PreferenceStores` groups durable preference stores and workers; `UiAnimationClock` owns animation scheduling; and `FontCatalogPrewarm` owns the one-shot font scan. Mouse buttons and tablet tips retain the owner that accepted their press, while `TouchTarget` binds touch sequences. Canvas motion and release keep that owner when crossing an inline toolbar; strip gaps consume presses and pen strip hover stays separate from layer-shell proximity. Each inline press refreshes hover geometry before claiming a control or gap. Strip pressure cannot change canvas thickness; a palette-consumed pen contact consumes its release even if the palette closes. A pen tap on the onboarding card, a toast, or an open popover acts on that chrome and never starts a stroke, and pen-up keeps a pressure thickness only for a stroke. Canceled canvas and pan gestures reconcile pointer ownership, while live held contacts keep their routing across Leave/Enter. The root retains cross-owner coordination. `handlers::route::SurfaceRouter` is the single classifier for pointer, touch, and stylus surfaces and supplies overlay screen coordinates before modality-specific dispatch. Within `PointerRuntime`, pending chrome targets and device-owned release suppression have separate lifecycles. Clearing a toast, HUD, or zoom-chip press preserves mouse and touch suppression; cancelling a touch clears only its own release latch. @@ -544,9 +544,54 @@ capture suppression operates on the paired resources without runtime pairing che - `src/daemon/protocol_v2/session_target.rs`: writes and reads the per-generation session reports in `daemon-commands/overlay-targets/`. - `src/daemon/`: accepts daemon-toggle requests that carry an optional named session target, and remembers the session its overlay last reported across hide and show. +The first startup load blocks once in the surface callback. Earlier input can +already exist; unresolved ink stays visible and cannot overwrite a saved file. +A per-output session first loads a placeholder without an output name. Leaving +it for the first output's own session also runs to completion in the callback, +after saving ink drawn on the placeholder to the placeholder's file. Later +retries and output switches use the shared session driver without waiting in +protocol dispatch. Unresolved sources cannot autosave or save before replacing +the canvas; at exit, their new drawings go to a `.unsaved-