2026 02 22_workbench_workspace_manifest_persistence_plan - mark-ik/graphshell GitHub Wiki

Workbench Workspace Manifest Persistence Plan

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)


Purpose

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 NodeKey persistence 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.

Terminology (Workbench / Workspace Framing)

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_tiles runtime 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.


Design Principles

  • Graphshell semantics must survive dependency-initiated structural normalization.
  • The egui_tiles workbench 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.

Problem Statement (Current Friction)

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.


Target Model (Clean Persistence Contract)

Core Principle

Persist stable graph identity (Node.id / UUID) and workbench structure separately. Resolve runtime NodeKey only during restore.

Persisted Named Workspace Bundle

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,
}

Workspace Layout (Workbench Structure Only)

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:

  • PaneId can be u64 (simple, local, diff-friendly) or Uuid (strong uniqueness).
  • u64 is preferred unless future cross-workspace pane identity is needed.

Workspace Manifest (Pane Identity + Membership)

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 Metadata (Planned Follow-On)

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:

  • WorkspaceLayout stays the structural workbench tree.
  • WorkspaceManifest stays 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.

Resolved Design Choices (2026-02-22)

  • 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.

Semantic Tabs vs Visual Tabs (Clarification)

  • Visual tabs: a current egui_tiles::Container::Tabs shape 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.

Promotion / Demotion Semantics (Planned)

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

Overlay Query Precedence (Best Practice)

For tab-aware features (omnibar saved tabs, pin UI, tab restore affordances, future tab queries):

  1. semantic tab metadata overlay
  2. live workbench tree shape inference
  3. pane-only fallback behavior

This preserves compatibility during rollout and prevents regressions when structural normalization changes tree shape.

Repair and Warning Policy (User-Facing Reliability)

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 vs Derived Membership

  • Declared membership: member_node_uuids persisted in manifest
  • Derived membership: recomputed from panes entries containing WebViewNode

Declared membership is the routing source of truth for fast, direct reads. Derived membership exists as a repair/check mechanism.

Workspace Metadata

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 Model (Ephemeral Resolution Only)

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:

  • NodeKey is runtime-only and never persisted in named workspace bundles.

Restore Flow (Named Workspace)

Algorithm

  1. Load PersistedWorkspace JSON.
  2. Validate bundle consistency:
    • layout pane IDs must exist in manifest panes
    • member_node_uuids must equal manifest-derived membership (or repair)
  3. Resolve each PaneContent::WebViewNode { node_uuid } via graph.id_to_node.
  4. Build runtime Tree<TileKind>:
    • PersistedPaneTile::Graph -> TileKind::Graph
    • PersistedPaneTile::Pane(id) -> manifest lookup -> resolved TileKind::WebView(NodeKey) if found
    • unresolved UUIDs are skipped
  5. If no usable panes remain (or tree root becomes empty), use existing workspace-routing fallback.
  6. 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.

Reliability Properties

  • missing nodes degrade gracefully (pane skipped)
  • no stale-key pruning pass needed for named workspaces
  • routing fallback behavior remains unchanged and well-defined

Save Flow (Named Workspace)

Algorithm

  1. Walk live runtime Tree<TileKind>.
  2. Convert runtime tiles into persisted pane tiles:
    • TileKind::Graph -> PersistedPaneTile::Graph
    • TileKind::WebView(node_key) -> allocate/retain PaneId, create PersistedPaneTile::Pane(pane_id)
  3. For each webview pane:
    • resolve node_key -> node_uuid
    • write manifest entry PaneId -> PaneContent::WebViewNode { node_uuid }
  4. Build declared membership (member_node_uuids) from manifest pane contents.
  5. Update metadata.updated_at_ms (and create metadata if new).
  6. Write PersistedWorkspace atomically via the active persistence backend's atomic commit semantics.
  7. Update in-memory membership cache directly from manifest membership (no re-scan required).

Pane ID Stability

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.


Membership Model (First-Class, Direct)

Source of Truth

For named workspaces:

  • WorkspaceManifest.member_node_uuids is the primary source of routing membership truth.

Runtime Cache

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

Membership Cache Trigger Centralization (Recommended)

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.

Repair / Integrity Check

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_uuids in memory with derived value,
  • optionally repair on next save.

Debug-mode option:

  • assert/fail fast to catch bugs during development.

Architecture Boundaries (Module Responsibilities)

app.rs (policy / routing / cache)

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 to Uuid-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_tiles trees

desktop/persistence_ops.rs (workspace bundle I/O + index rebuild)

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

Integration Constraints (Stage 8 Tab Semantics Overlay)

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.

1. Intent Boundary Discipline (Prevent Boundary Creep)

  • render/* captures UI events and renders state only; it must not directly mutate tab overlay semantics.
  • app.rs remains 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.

2. Source-of-Truth Precedence (Avoid Duplicate Semantics)

For tab-aware behavior (omnibar saved tabs, pin UI, restore affordances, future tab queries):

  1. semantic tab overlay metadata
  2. live workbench tree shape inference
  3. 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

3. Repair Warning Aggregation (Avoid UX Noise)

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.

4. Async / Lifecycle Ordering (Avoid Races)

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.

5. Dependency Compatibility Constraint

  • 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.

desktop/gui_frame.rs (restore/save orchestration)

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)

desktop/tile_kind.rs + tile runtime modules (runtime workbench only)

Owns:

  • runtime TileKind used for live rendering (Graph, WebView(NodeKey))

Does not own:

  • long-lived named workspace persistence schema

API Sketch (Proposed)

Persistence Types

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;

Desktop Persistence Operations

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>;

Validation / Repair Helpers

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);

File Format and Compatibility Policy

Versioning

  • Add version field to PersistedWorkspace.
  • Initial version for this format: 1.

Compatibility Policy (Explicit)

  • 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.

Atomicity

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.


Staged Delivery Plan (Execution-Ready)

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.

Stage 0: Preflight (One-Time Cutover Setup)

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, or delete-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 redb workspace-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.
  • Legacy startup handling policy:
    • ignore by 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)

Stage 1: Schema + Serialization

Goal:

  • introduce the new named-workspace bundle schema and validation helpers without changing runtime behavior

Primary file targets:

  • desktop/persistence_ops.rs (or a new desktop/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

Stage 2: Runtime Conversion (Named Workspace Save/Restore)

Depends on:

  • Stage 1

Goal:

  • convert named workspace save/restore paths to bundle format while keeping runtime TileKind unchanged

Primary file targets:

  • desktop/persistence_ops.rs
  • desktop/gui_frame.rs
  • any named-workspace save/restore callsites in app.rs/render orchestration (if touched)

Tasks:

  • Implement live Tree<TileKind> -> PersistedWorkspace conversion
  • Implement PersistedWorkspace -> runtime Tree<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 redb transactional 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

Stage 3: Membership Index Switch (Manifest-Based)

Depends on:

  • Stages 1-2

Goal:

  • make named-workspace membership direct (manifest-based) and centralize membership cache refresh

Primary file targets:

  • desktop/persistence_ops.rs
  • desktop/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

Stage 4: Legacy Named-Workspace NodeKey Path Removal + Documentation Alignment

Depends on:

  • Stages 1-3

Goal:

  • remove obsolete named-workspace NodeKey persistence assumptions and reduce conceptual debt

Primary file targets:

  • desktop/persistence_ops.rs
  • desktop/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

Stage 5 (Optional): Unify Session Autosave / Pins on Bundle Format

Depends on:

  • Stages 1-4

Goal:

  • use the same workbench workspace bundle model for reserved/session/pin workspaces

Primary file targets:

  • desktop/persistence_ops.rs
  • desktop/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)

Stage 6 (Recommended Follow-On): UUID-Keyed Workspace Recency for Routing

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) to Uuid -> (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

Stage 7 (Optional): Resolver Explainability / Debug Trace

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.rs
  • desktop/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 Follow-On (Moved)

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.

Recommended Execution Order / Stop Points

  1. Stage 0
  2. Stage 1
  3. Stage 2
  4. Stage 3
  5. Stage 4
  6. Stop and stabilize (manual validation + regression tests)
  7. Stage 6 (recommended)
  8. Stage 7 (optional)
  9. 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.

Validation Checklist

  1. Named workspace save persists UUID-based pane contents, not NodeKey.
  2. Named workspace restore after restart resolves UUIDs to current-session NodeKeys correctly.
  3. Missing node UUID in manifest skips pane without panic.
  4. All panes unresolved triggers existing restore fallback behavior.
  5. Membership startup build reads manifest membership directly (no stale-key prune pass needed).
  6. Workspace delete removes named workspace from membership cache immediately.
  7. Retention batch operations rebuild membership from manifests only.
  8. Manifest membership drift is detected and repaired (or warned) deterministically.
  9. Atomic write failure never leaves a truncated valid-looking workspace bundle file.
  10. No legacy loader path: old NodeKey-based named workspace files are rejected/ignored by policy.
  11. Membership cache refresh path is centralized for named-workspace persistence mutations.
  12. (If Stage 6 implemented) UUID recency routing preserves deterministic selection across restarts.
  13. (If Stage 7 implemented) Resolver tracing logs the decision path (preferred/recent/fallback) and restore failure/fallback reasons.
  14. (See Stage 8 follow-on plan) Simplify-safe tab semantics preserve tab discoverability and tab metadata roundtrip independent of tree-shape collapse.
  15. (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.

Implementation Progress Snapshot (2026-02-22)

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

Risks and Mitigations

Risk: Conversion layer complexity (runtime tree <-> persisted bundle)

Mitigation:

  • keep conversion code in one desktop persistence module
  • add focused roundtrip tests
  • keep runtime TileKind unchanged initially

Risk: Pane ID instability causes noisy diffs / future metadata churn

Mitigation:

  • optional pane ID preservation on resave
  • deterministic pane ID allocation strategy

Risk: Membership declared/derived mismatch introduces confusion

Mitigation:

  • explicit terminology (declared vs derived)
  • validation + repair helper
  • warn and repair on save/load paths

Risk: Format break surprises users (no backward loader)

Mitigation:

  • explicit release note / changelog entry
  • one-shot reset command or startup warning for legacy workspace files

Design Decisions (Resolved)

  1. No backward-compatible loader for legacy NodeKey-based named workspaces.
  2. Named workspaces use stable node UUID identity.
  3. Workbench layout and pane content identity are persisted separately.
  4. Membership is first-class persisted metadata in the workspace manifest.
  5. Runtime NodeKey remains runtime-only and is resolved on restore.

Suggested Follow-On Doc Updates

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)
⚠️ **GitHub.com Fallback** ⚠️