2026 02 22_workbench_workspace_manifest_persistence_plan - mark-ik/graphshell GitHub Wiki
Status: Completed (2026-02-22). Archived 2026-02-24.
Follow-on: ../../graphshell_docs/implementation_strategy/2026-02-22_workbench_tab_semantics_overlay_and_promotion_plan.md
Relates to: 2026-02-19_workspace_routing_and_membership_plan.md (same archive checkpoint)
Replace named-workspace persistence that embeds session-local NodeKey values with a stable-ID
model based on node UUIDs and a manifest-backed workbench workspace format.
This removes the current indirectness:
- no
NodeKeypersistence in named workspaces, - no stale-key pruning as a prerequisite for named-workspace membership derivation,
- direct membership reads from persisted workspace metadata,
- cleaner separation between UI layout structure and graph content identity.
This plan adopts terminology aligned with the existing mental model:
- Workbench: the tiling system and tile-tree UI framework (the "IDE Window")
- Workspace: one persisted or live arrangement shown in the workbench (a "Project Context")
-
Live Workspace: the current in-memory workbench state (
egui_tilesruntime tree) - Named Workspace: a persisted workspace bundle (layout + manifest + metadata)
- Synthesized Workspace: an unsaved live workspace generated by open modes (neighbors/connected)
- Workspace Layout: persisted workbench structure only (pane arrangement)
- Workspace Manifest: persisted pane identity/content mapping + membership metadata
- Restore Workspace: load a named workspace bundle and build runtime workbench tiles
- Activate Workspace: record recency and runtime bookkeeping after successful restore
- Graph: The content identity layer (the "File System")
This keeps the spirit of the prior terminology (live workspace, named workspace) while making
the workbench/layout distinction explicit.
- Graphshell semantics must survive dependency-initiated structural normalization.
- The
egui_tilesworkbench tree is structural state, not semantic truth. - Semantic metadata is optional and additive for tabs and panes.
- Restore prioritizes content availability first, semantic exactness second, exact structural shape third.
- Promotion/demotion between pane and tab-container forms is first-class behavior, not an exception path.
- Graphshell should remain compatible with dependency-native transforms (including
simplify()) by persisting app semantics explicitly instead of relying on incidental tree shape.
Named workspaces currently persist TileKind::WebView(NodeKey) in the tile tree. NodeKey
(petgraph::NodeIndex) is session-local and not stable across restarts.
Consequences:
- restore requires stale-key pruning before the tile tree is safe to use,
- membership must be derived indirectly after prune,
- membership correctness depends on runtime graph state and desktop-layer tile parsing,
- layout persistence leaks runtime graph handles into long-lived storage.
This is safe today (mitigated by pruning + fallbacks), but indirect and harder to reason about.
Persist stable graph identity (Node.id / UUID) and workbench structure separately.
Resolve runtime NodeKey only during restore.
Named workspace persistence becomes a single schema versioned bundle:
pub struct PersistedWorkspace {
pub version: u32,
pub name: String,
pub layout: WorkspaceLayout,
pub manifest: WorkspaceManifest,
pub metadata: WorkspaceMetadata,
}The layout stores pane arrangement (tabs/splits) and pane identifiers, not graph node handles.
pub type PaneId = u64;
pub enum PersistedPaneTile {
Graph,
Pane(PaneId),
}
pub struct WorkspaceLayout {
pub tree: egui_tiles::Tree<PersistedPaneTile>,
}Notes:
-
PaneIdcan beu64(simple, local, diff-friendly) orUuid(strong uniqueness). -
u64is preferred unless future cross-workspace pane identity is needed.
pub struct WorkspaceManifest {
pub panes: std::collections::BTreeMap<PaneId, PaneContent>,
pub member_node_uuids: std::collections::BTreeSet<uuid::Uuid>,
}
pub enum PaneContent {
Graph,
WebViewNode { node_uuid: uuid::Uuid },
}Tab semantics should become first-class persisted metadata. This aligns with the workbench/workspace
manifest model and avoids Graphshell depending on incidental egui_tiles tree shape for tab-aware
behavior.
Why:
- a single-tab container can still carry Graphshell-specific meaning (saved-tab discovery, tab UI affordances, future tab-local state),
-
egui_tiles::simplify()may collapse a single-tab container into a pane and discard that shape, - Graphshell should not impede normal structural simplification in dependencies when semantics can be persisted explicitly.
Minimum planned extension (illustrative shape, not final schema):
pub struct WorkspaceTabSemantics {
pub tab_groups: Vec<TabGroupMetadata>,
}
pub struct TabGroupMetadata {
pub group_id: u64, // or Uuid
pub pane_ids: Vec<PaneId>, // tab order
pub active_pane_id: Option<PaneId>,
}Design intent:
-
WorkspaceLayoutstays the structural workbench tree. -
WorkspaceManifeststays the pane identity + membership source of truth. - tab-specific semantics move into explicit metadata so restore can reapply them even if structure was simplified.
Planned UX implication (single-pane case):
- it is acceptable for a tab to be represented as a plain pane until tab-specific handling is requested,
- a small inverted tab button can appear at the pane viewport margin on hover proximity,
- clicking it re-wraps the pane into a tab container and reloads tab metadata for normal handling.
- A semantic tab is any tab/tab-group with saved tab metadata.
- Tab-aware features use overlay metadata first, then tree-shape inference fallback.
- A single pane with semantic tab metadata counts for all tab features (omnibar discovery, pin UI, restore, keyboard/navigation, etc.), even when currently unhoisted.
- Graph panes and webview panes are both eligible for semantic tab/group membership.
- A pane may belong to only one semantic tab group at a time (historical reassignment is allowed).
- Invalid tab metadata is repaired and surfaced with a clear, exact user-facing warning.
-
Visual tabs: a current
egui_tiles::Container::Tabsshape in the live workbench tree. - Semantic tabs: Graphshell tab/group meaning persisted in overlay metadata.
They often align, but they do not need to be identical at every moment:
- a semantic tab/group may temporarily rest as a plain pane after structural simplification or inactivity,
- a visual tabs container may exist without persisted semantic metadata (fallback behavior).
This distinction is what allows Graphshell to remain dependency-compatible while preserving tab-specific behavior.
Use semantic lifecycle terminology distinct from structural transforms:
-
Demote: semantic tab/tab-group -> pane rest state
- persist tab metadata
- allow unhoist/simplify of tab container
- preserve visible content (including last painted render / thumbnail-style fallback when runtime content is suspended)
-
Promote: pane rest state -> semantic tab/tab-group
- re-wrap pane into a tabs container
- reapply tab metadata (group membership, order, active pane)
- restore normal tab interactions
Related structural terms:
- Hoist / Unhoist refer only to workbench tree shape changes.
- Promote / Demote refer to semantic + structural behavior (and may trigger hoist/unhoist).
For tab-aware features (omnibar saved tabs, pin UI, tab restore affordances, future tab queries):
- semantic tab metadata overlay
- live workbench tree shape inference
- pane-only fallback behavior
This preserves compatibility during rollout and prevents regressions when structural normalization changes tree shape.
Validation/repair must not contradict user needs. Default release behavior should preserve content and usability, then repair semantics:
- repair invalid metadata automatically when safe,
- preserve visible panes/content even if some tab metadata is dropped or corrected,
- surface a clear, exact warning message when user-visible tab behavior changes.
Warning messages should include:
- workspace name
- affected tab group / pane IDs
- exact repair action taken
- what was preserved
- what may have changed visually or behaviorally
Example (illustrative):
Workspace 'research-1': repaired tab group g42 (missing active pane p9). Preserved panes [p3,p7]; active tab reset to p3.
-
Declared membership:
member_node_uuidspersisted in manifest -
Derived membership: recomputed from
panesentries containingWebViewNode
Declared membership is the routing source of truth for fast, direct reads. Derived membership exists as a repair/check mechanism.
pub struct WorkspaceMetadata {
pub created_at_ms: u64,
pub updated_at_ms: u64,
pub last_activated_at_ms: Option<u64>,
}Optional future extensions:
- user labels/tags
- provenance (
synthesized_from, traversal seed) - workspace-local preferences
Runtime workbench rendering still uses TileKind::WebView(NodeKey), but only after resolve.
pub struct WorkspaceRuntimeResolution {
pub pane_to_node_key: std::collections::HashMap<PaneId, NodeKey>,
pub unresolved_panes: Vec<PaneId>,
}Key rule:
-
NodeKeyis runtime-only and never persisted in named workspace bundles.
- Load
PersistedWorkspaceJSON. - Validate bundle consistency:
- layout pane IDs must exist in manifest
panes -
member_node_uuidsmust equal manifest-derived membership (or repair)
- layout pane IDs must exist in manifest
- Resolve each
PaneContent::WebViewNode { node_uuid }viagraph.id_to_node. - Build runtime
Tree<TileKind>:-
PersistedPaneTile::Graph->TileKind::Graph -
PersistedPaneTile::Pane(id)-> manifest lookup -> resolvedTileKind::WebView(NodeKey)if found - unresolved UUIDs are skipped
-
- If no usable panes remain (or tree root becomes empty), use existing workspace-routing fallback.
- Activate workspace:
- update recency
- update runtime membership cache if maintained
- clear unsaved synthesized flags as appropriate
Follow-on (tab semantics metadata):
- allow structural
simplify()during restore, - then reapply semantic tab-group wrapping from persisted tab metadata when needed.
- allow panes to remain in a valid semantic rest state until promoted by user interaction.
- missing nodes degrade gracefully (pane skipped)
- no stale-key pruning pass needed for named workspaces
- routing fallback behavior remains unchanged and well-defined
- Walk live runtime
Tree<TileKind>. - Convert runtime tiles into persisted pane tiles:
-
TileKind::Graph->PersistedPaneTile::Graph -
TileKind::WebView(node_key)-> allocate/retainPaneId, createPersistedPaneTile::Pane(pane_id)
-
- For each webview pane:
- resolve
node_key -> node_uuid - write manifest entry
PaneId -> PaneContent::WebViewNode { node_uuid }
- resolve
- Build declared membership (
member_node_uuids) from manifest pane contents. - Update
metadata.updated_at_ms(and create metadata if new). - Write
PersistedWorkspaceatomically via the active persistence backend's atomic commit semantics. - Update in-memory membership cache directly from manifest membership (no re-scan required).
Preferred behavior for repeated saves of the same named workspace:
- preserve existing
PaneIds when a pane still represents the same node UUID in the same save cycle - allocate new IDs only for newly added panes
This is optional for v1, but improves diffability and future pane-local metadata support.
For named workspaces:
-
WorkspaceManifest.member_node_uuidsis the primary source of routing membership truth.
Keep the existing app-level UUID-keyed membership cache, but change how it is populated:
- startup: read manifests only
- named workspace save/delete: update cache from manifest / remove workspace directly
- batch retention ops: rebuild from manifests only
To avoid drift in future workspace features, provide one desktop-layer helper for membership cache refresh/update paths (full rebuild and targeted updates) and route all named-workspace persistence mutations through it.
This folds in the "centralize membership rebuild triggers" follow-on from the workspace routing plan.
On workspace load (or during background maintenance), compute:
derived_membership = panes.values().filter_map(WebViewNode.uuid)
If derived_membership != member_node_uuids:
- log warning,
- overwrite
member_node_uuidsin memory with derived value, - optionally repair on next save.
Debug-mode option:
- assert/fail fast to catch bugs during development.
Owns:
- workspace routing resolver (
resolve_workspace_open) - recency policy and activation tracking
- workspace membership cache (
Uuid -> BTreeSet<WorkspaceName>) - unsaved synthesized workspace state
Recommended upgrade during this migration:
- move workspace recency tracking for resolver selection from
NodeKey-keyed storage toUuid-keyed storage so recency survives restarts and matches manifest identity keys
Does not own:
- parsing persisted workbench layout/manifest schemas
- converting persisted layout to runtime
egui_tilestrees
Owns:
- read/write
PersistedWorkspace - manifest-based membership-index rebuilds
- centralized membership cache update/rebuild helper(s)
- batch retention operations over named workspaces
- bundle integrity validation/repair helpers
The tab-semantics overlay must integrate with the existing architecture without creating new semantic mutation paths, duplicate sources of truth, warning spam, or lifecycle races.
Context (why these constraints exist):
- Current workspace/pin/omnibar behavior was shipped incrementally across multiple stages and features before a unified semantic tab overlay existed.
- The codebase is also in a migration window (raw
NodeKey-based workspace persistence -> manifest bundles), so mixed patterns are expected temporarily. - These constraints are intended to guide targeted seam refactors, not justify a broad rewrite.
Refactor guidance:
- Prefer selective refactors at semantic seams (tab-aware queries, promotion/demotion APIs, restore/repair flows).
- Do not perform a global cleanup pass unrelated to Stage 8 scope.
- When touching a tab-aware path, route it toward shared overlay-first helpers instead of adding another direct tree-shape inference path.
-
render/*captures UI events and renders state only; it must not directly mutate tab overlay semantics. -
app.rsremains the authority for semantic decisions (promotion/demotion intents, repair policy, warning intent creation). -
desktop/*applies workbench tree mutations and runtime effects (rewrap, restore, lifecycle coordination) in response to app-level intents. - Promotion/demotion must be implemented as explicit operations/intents, not ad hoc tree rewrites scattered across UI callsites.
For tab-aware behavior (omnibar saved tabs, pin UI, restore affordances, future tab queries):
- semantic tab overlay metadata
- live workbench tree shape inference
- pane-only fallback behavior
Implementation rules:
- introduce shared semantic query helpers and route consumers through them,
- avoid mixing direct tree inspection and overlay reads outside the defined fallback sequence,
- preserve separation of responsibilities:
-
WorkspaceLayout= structural workbench tree -
WorkspaceManifest= pane identity + membership - tab overlay metadata = tab semantics
-
Repair behavior should preserve content and usability first, but user-facing warnings must be coalesced to avoid spam.
Rules:
- aggregate tab metadata repairs per workspace restore/load operation,
- emit detailed repair information to debug logs,
- emit at most one user-facing warning per workspace restore/load operation by default,
- warning text must be exact: workspace name, affected IDs, repair action, what was preserved, and what changed visually/behaviorally.
Promotion/demotion may interact with webview lifecycle transitions (active/warm/cold) and async
runtime resume/suspend work. Ordering must be explicit.
Required ordering model:
- semantic decision (app intent)
- workbench tree mutation (desktop apply)
- lifecycle/runtime request dispatch (resume/suspend as needed)
- readiness/UI state update when runtime confirms availability
Rules:
- promotion/demotion operations must be idempotent and safe to retry,
- pane rest states remain valid UI states while runtime content is suspended/resuming,
- semantic overlay repair must not require synchronous runtime availability.
- Stage 8 must preserve compatibility with dependency-native structural transforms (including
egui_tiles::simplify()). - Graphshell semantics should be restored/reapplied through overlay metadata and promotion logic, not by globally disabling dependency behavior.
Owns:
- invoking restore/save operations in frame sequencing
- applying restore fallback behavior on empty resolved workbench trees
- calling workspace activation after successful restore
- optional routing/debug logging at restore boundary (chosen workspace, fallback reason)
Owns:
- runtime
TileKindused for live rendering (Graph,WebView(NodeKey))
Does not own:
- long-lived named workspace persistence schema
pub struct PersistedWorkspace { /* version/name/layout/manifest/metadata */ }
pub struct WorkspaceLayout { /* Tree<PersistedPaneTile> */ }
pub struct WorkspaceManifest { /* panes + member_node_uuids */ }
pub struct WorkspaceMetadata { /* timestamps */ }
pub enum PaneContent { Graph, WebViewNode { node_uuid: Uuid } }
pub enum PersistedPaneTile { Graph, Pane(PaneId) }
pub type PaneId = u64;pub(crate) fn load_named_workspace_bundle(
graph_app: &GraphBrowserApp,
name: &str,
) -> Result<PersistedWorkspace, String>;
pub(crate) fn save_named_workspace_bundle(
graph_app: &GraphBrowserApp,
name: &str,
live_tree: &egui_tiles::Tree<TileKind>,
) -> Result<(), String>;
pub(crate) fn restore_runtime_tree_from_workspace_bundle(
graph_app: &GraphBrowserApp,
bundle: &PersistedWorkspace,
) -> Result<(egui_tiles::Tree<TileKind>, Vec<NodeKey>), String>;
pub(crate) fn build_membership_index_from_workspace_manifests(
graph_app: &GraphBrowserApp,
) -> std::collections::HashMap<Uuid, std::collections::BTreeSet<String>>;
pub(crate) fn refresh_workspace_membership_cache_from_manifests(
graph_app: &mut GraphBrowserApp,
) -> Result<(), String>;pub(crate) fn derive_membership_from_manifest(
manifest: &WorkspaceManifest,
) -> std::collections::BTreeSet<Uuid>;
pub(crate) fn validate_workspace_bundle(bundle: &PersistedWorkspace) -> Result<(), WorkspaceBundleError>;
pub(crate) fn repair_manifest_membership(bundle: &mut PersistedWorkspace);- Add
versionfield toPersistedWorkspace. - Initial version for this format:
1.
- No backward-compatible loader for legacy NodeKey-based named workspace layouts.
- On rollout, named workspaces are treated as a format break.
- Old named workspace files may be ignored, deleted, or replaced by a migration/reset command.
This is intentional and simplifies implementation + reliability.
Named workspace bundle writes must use atomic persistence semantics.
Current implementation note:
- named workspaces are persisted in
redb(TILE_LAYOUT_TABLE) via transactional write + commit - this provides the relevant atomicity guarantee for the current backend
- filesystem temp-file + rename is only required if/when bundle persistence moves to direct files
This is routing-critical metadata, so partial writes are more costly than before.
This is the recommended implementation order. Stages 1-4 are the core migration. Stages 5-7 are follow-ons that become easier and cleaner once the manifest model is in place.
Goal:
- prepare a clean format break and reduce ambiguity during implementation
File targets:
desktop/persistence_ops.rs- user-facing release notes/changelog location (project-specific)
Tasks:
- Define the on-disk location/filename convention for named workspace bundles
- Decide legacy-file handling policy on startup (
ignore,warn, ordelete-on-command) - Resolve temporary startup warning/log policy for legacy NodeKey-based named workspaces (optional item)
- Add a short developer note explaining that no backward loader will be added
Status (current):
- Backend already stores named workspaces transactionally in
redb; no direct-file cutover required. - Legacy compatibility remains intentionally unsupported by loader policy.
- Stage 0 decisions were recorded retroactively after implementation to close migration ambiguity.
Resolved preflight decisions (retroactive):
-
Named workspace bundle storage convention:
- Bundles are stored in the existing
redbworkspace-layout value slot (save_workspace_layout_json/load_workspace_layout_json) under the existing workspace name keys. - There is no separate filesystem filename convention in the current backend; the persistence key
(
workspace name) is the logical bundle identifier.
- Bundles are stored in the existing
-
Legacy startup handling policy:
-
ignoreby default (no backward loader; legacy NodeKey-based named workspaces are treated as unsupported persisted payloads and skipped by bundle loaders). - No automatic delete-on-startup behavior.
-
-
Temporary startup warning/log policy (optional item):
- deferred / not implemented in code during the migration window to avoid noisy false positives and repeated warnings from generic bundle-load callsites.
- legacy compatibility break is instead documented explicitly in this plan and follow-on release notes.
-
Developer note (no backward loader):
- This migration intentionally does not include a backward-compatible loader for legacy NodeKey-based named workspace payloads.
- The format break is accepted to remove session-local identity persistence and simplify the manifest-based model.
Acceptance gates:
- format-break behavior is explicit before code migration starts
- no hidden compatibility assumptions remain in comments/docs
Retroactive closure note:
- Although recorded after implementation, these decisions now make the format-break behavior and compatibility policy explicit for maintainers and release notes.
Ship value:
- none (preparation only)
Goal:
- introduce the new named-workspace bundle schema and validation helpers without changing runtime behavior
Primary file targets:
-
desktop/persistence_ops.rs(or a newdesktop/workspace_persistence.rs) - tests in
desktop/persistence_ops.rs(or dedicated test module)
Tasks:
- Add
PersistedWorkspace,WorkspaceLayout,WorkspaceManifest,WorkspaceMetadata - Add
PersistedPaneTile,PaneContent,PaneId - Implement serde serialization/deserialization roundtrip tests
- Implement
derive_membership_from_manifest(...) - Implement
validate_workspace_bundle(...) - Implement
repair_manifest_membership(...)
Acceptance gates:
- roundtrip serialization tests pass
- validation catches layout/manifest mismatches deterministically
- repair helper produces stable output from known-bad inputs
Ship value:
- internal only; no runtime path changes
Depends on:
- Stage 1
Goal:
- convert named workspace save/restore paths to bundle format while keeping runtime
TileKindunchanged
Primary file targets:
desktop/persistence_ops.rsdesktop/gui_frame.rs- any named-workspace save/restore callsites in
app.rs/render orchestration (if touched)
Tasks:
- Implement live
Tree<TileKind>->PersistedWorkspaceconversion - Implement
PersistedWorkspace-> runtimeTree<TileKind>resolution via UUID lookups - Skip unresolved UUID panes during restore
- Return resolved node list for workspace activation bookkeeping
- Preserve existing restore fallback behavior on empty/unusable resolved workbench trees
- Switch named workspace save path to write bundle format atomically (via
redbtransactional commit) - Switch named workspace restore path to read/resolve bundle format
Acceptance gates:
- named workspace save/restore works using UUID bundle persistence only
- restore succeeds across restart-like scenarios without named-workspace stale-key prune dependency
- unresolved-node panes are skipped without panic
Recommended tests:
- save bundle contains UUID pane identities (no
NodeKey) - restore skips missing UUID pane and still restores remaining panes
- restore-empty triggers existing fallback path
Ship value:
- major reliability improvement for named workspaces
Depends on:
- Stages 1-2
Goal:
- make named-workspace membership direct (manifest-based) and centralize membership cache refresh
Primary file targets:
desktop/persistence_ops.rsdesktop/gui_frame.rs-
app.rs(cache init/update callsites only)
Tasks:
- Add
build_membership_index_from_workspace_manifests(...) - Add
refresh_workspace_membership_cache_from_manifests(...) - Switch startup membership initialization to manifest reads
- Switch batch retention rebuild paths to centralized manifest refresh helper
- Switch named workspace save/delete paths to use targeted cache updates or centralized refresh helper
- Update tests that previously depended on stale-key-pruned named-workspace layout scans
Acceptance gates:
- membership index correctness tests pass using manifest-based inputs
- named-workspace membership rebuild path no longer parses runtime
TileKind::WebView(NodeKey) - membership cache refresh/update paths are centralized and easy to audit
Recommended tests:
- manifest-based startup rebuild excludes reserved workspaces as intended
- prune-empty / keep-latest rebuilds membership via manifest helper
- workspace delete immediately removes cache membership entries
Ship value:
- direct membership correctness, less indirect persistence coupling
Depends on:
- Stages 1-3
Goal:
- remove obsolete named-workspace NodeKey persistence assumptions and reduce conceptual debt
Primary file targets:
desktop/persistence_ops.rsdesktop/gui_frame.rs- workspace routing docs + comments
Tasks:
- Remove legacy named-workspace NodeKey layout loaders/parsers from primary runtime paths
- Remove named-workspace stale-key-prune membership derivation code from primary runtime paths
- Keep stale-key pruning only for runtime/session paths that still use runtime
TileKind - Update docs/comments to workbench/workspace terminology
- Update workspace routing docs/comments to reference manifest-based named-workspace membership
Remaining cleanup:
- Remove or archive legacy-format transition test helpers if old-format fixture coverage is no longer desired
Acceptance gates:
- named workspace persistence no longer stores or reads
NodeKey - workspace routing docs/comments are aligned with manifest model
- no dead code remains for named-workspace stale-key membership derivation
Ship value:
- conceptual and maintenance simplification
Depends on:
- Stages 1-4
Goal:
- use the same workbench workspace bundle model for reserved/session/pin workspaces
Primary file targets:
desktop/persistence_ops.rsdesktop/gui_frame.rs- reserved workspace helpers in
app.rs/ render flows (if touched)
Tasks:
- Convert reserved workspace save/restore paths to bundle format (session autosave + pin save/restore paths now use named-workspace bundle flows)
- Revisit which reserved workspaces participate in membership cache (reserved workspaces remain excluded by manifest membership rebuild policy)
- Ensure retention actions continue to exclude reserved workspaces by policy (covered by filter + regression test)
- Add Stage 5 regression coverage for session autosave bundle hashing/rotation behavior
- Add startup restore precedence coverage (session bundle preferred over legacy
tile_layout_json) - Make pin active-state comparisons bundle-aware (compare restored runtime layout, not persisted blob string)
Acceptance gates:
- reserved workspace behavior unchanged for users
- no special-case serialization format remains for workbench persistence (if fully unified)
- startup/session restore preference is deterministic and tested (bundle first, legacy fallback)
Depends on:
- Prefer after Stage 3 (manifest identity in use)
Goal:
- align routing recency persistence with UUID identity and remove session-local recency loss
Primary file targets:
app.rs- tests in
app.rs - any persistence path where recency is stored (if persisted)
Tasks:
- Change resolver recency storage from
NodeKey -> (seq, workspace)toUuid -> (seq, workspace) - Migrate
note_workspace_activated(...)and resolver lookup paths - Preserve deterministic alphabetical fallback when no recency match exists
- Add/adjust tests for recency selection across restart-like scenarios
- Seed startup recency from workspace bundle metadata (
last_activated_at_ms) - Update named workspace restore path to persist activation metadata
Acceptance gates:
- resolver selection behavior unchanged within-session
- recency preference survives restarts when persisted state is available
Depends on:
- none (can be done anytime), but easiest after Stages 3-4 when persistence paths are simplified
Goal:
- improve diagnosability of workspace routing while preserving single-authority resolver semantics
Primary file targets:
app.rsdesktop/gui_frame.rs
Tasks:
- Add structured debug logging for resolver decisions (preferred match, recency match, fallback)
- Log restore fallback reasons (missing workspace, unresolved panes, empty resolved workbench)
- Add a minimal test/assertion surface for deterministic resolver reason ordering (if represented)
Acceptance gates:
- routing surprises can be diagnosed from logs without stepping through UI event paths
Stage 8 (tab semantics overlay / promotion semantics / simplify-safe restore) has been split into a dedicated follow-on plan:
2026-02-22_workbench_tab_semantics_overlay_and_promotion_plan.md
Reason for split:
- Stage 8 grew into a distinct architecture effort (semantic overlay subsystem, promotion/demotion lifecycle, query APIs, repair/warning UX policy, simplify-safe restore integration) that now merits its own plan and execution sequence.
- Stage 0
- Stage 1
- Stage 2
- Stage 3
- Stage 4
- Stop and stabilize (manual validation + regression tests)
- Stage 6 (recommended)
- Stage 7 (optional)
- Stage 5 (optional, can also be deferred indefinitely)
Rationale:
- Stages 2-4 deliver the core reliability win.
- Stage 6 is the next highest-value routing improvement and aligns with manifest UUID identity.
- Stage 5 expands scope into reserved/session workspaces and is not required to realize the main design benefits.
- Stage 8 was split into a dedicated follow-on plan after the manifest migration was completed.
-
Named workspace save persists UUID-based pane contents, not
NodeKey. -
Named workspace restore after restart resolves UUIDs to current-session
NodeKeys correctly. - Missing node UUID in manifest skips pane without panic.
- All panes unresolved triggers existing restore fallback behavior.
- Membership startup build reads manifest membership directly (no stale-key prune pass needed).
- Workspace delete removes named workspace from membership cache immediately.
- Retention batch operations rebuild membership from manifests only.
- Manifest membership drift is detected and repaired (or warned) deterministically.
- Atomic write failure never leaves a truncated valid-looking workspace bundle file.
- No legacy loader path: old NodeKey-based named workspace files are rejected/ignored by policy.
- Membership cache refresh path is centralized for named-workspace persistence mutations.
- (If Stage 6 implemented) UUID recency routing preserves deterministic selection across restarts.
- (If Stage 7 implemented) Resolver tracing logs the decision path (preferred/recent/fallback) and restore failure/fallback reasons.
- (See Stage 8 follow-on plan) Simplify-safe tab semantics preserve tab discoverability and tab metadata roundtrip independent of tree-shape collapse.
- (See Stage 8 follow-on plan) Promotion semantics support pane rest states and on-demand rewrap/promotion with exact repair warnings when metadata is corrected.
Implemented in code:
- Stage 1 (schema + validation/repair helpers)
- Stage 2 (primary named-workspace save/restore conversion)
- Stage 3 (manifest-based membership init/rebuild in primary paths)
- Stage 4 (fully cleaned: legacy named-workspace transition helper/tests removed)
- Stage 6 (UUID-keyed recency + startup recency seeding from bundle metadata)
- Stage 7 (debug tracing + resolver reason assertion surface)
- Stage 5 partial:
- session autosave writes bundle payloads with runtime-layout hash gating
- session history rotation preserves bundle payloads
- startup session restore prefers bundle over legacy
tile_layout_json(tested) - pin active-state/load-picker comparisons are bundle-aware via runtime restore comparison
- reserved-workspace retention exclusion policy covered by regression test
Moved to follow-on plan:
- Stage 8 tab semantics overlay / promotion semantics / simplify-safe restore
Mitigation:
- keep conversion code in one desktop persistence module
- add focused roundtrip tests
- keep runtime
TileKindunchanged initially
Mitigation:
- optional pane ID preservation on resave
- deterministic pane ID allocation strategy
Mitigation:
- explicit terminology (
declaredvsderived) - validation + repair helper
- warn and repair on save/load paths
Mitigation:
- explicit release note / changelog entry
- one-shot reset command or startup warning for legacy workspace files
- No backward-compatible loader for legacy NodeKey-based named workspaces.
- Named workspaces use stable node UUID identity.
- Workbench layout and pane content identity are persisted separately.
- Membership is first-class persisted metadata in the workspace manifest.
-
Runtime
NodeKeyremains runtime-only and is resolved on restore.
After implementation begins, update:
-
../../archive_docs/checkpoint_2026-02-22/2026-02-19_workspace_routing_and_membership_plan.md- replace "derived from layout JSON + stale-key prune" language for named workspaces
- adopt workbench/workspace terminology
- point persistence internals and membership rebuild details to this manifest plan
- validation docs for named workspace persistence format break
- persistence architecture docs to include workspace bundle schema
-
2026-02-22_workbench_tab_semantics_overlay_and_promotion_plan.md- keep Stage 8 tab overlay/promotion work and simplify-safe restore work there (not in this doc)