Skip to content

RecoveryStore — Use-Case Sequence Diagrams

Actors in all diagrams:

Actor Description
Runtime Runtime::execute_command
RuntimeSession RuntimeSession — orchestrates ClientSession
ClientSession ClientSession — owns TransactionContext + LocalRecoveryReceptor
Receptor LocalRecoveryReceptor — async wrapper around the store
Store TransactionRecoveryStore — synchronous SQLite operations
DB SQLite (recovery_session, recovery_checkpoint, experience_unit)

1. persist — intermediate mutation (crash-recovery only)

Command with snapshot_after=false, disable_undo=false. Writes a sentinel checkpoint at stack_pos=-1 so the process can restart mid-transaction, but creates no ExperienceUnit.

sequenceDiagram
    participant Runtime
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    Runtime->>RuntimeSession: persist_success(tx_id, label, policy{snapshot_after=false})
    RuntimeSession->>ClientSession: persist(desc, disable_undo=false, snapshot_after=false, ...)
    ClientSession->>Receptor: persist(context, ...)
    Receptor->>Store: persist(context, ...) [spawn_blocking]
    Store->>DB: snapshot = TransactionSnapshot::from_context
    Store->>DB: UPSERT recovery_session SET latest_checkpoint_id=<new>
    Store->>DB: INSERT OR REPLACE recovery_checkpoint (stack_pos=-1)
    note over Store,DB: redo_stack empty? skip redo-clear
    note over Store,DB: disable_undo=false, snapshot_after=false → intermediate path
    Store->>DB: COMMIT
    Store-->>Receptor: Ok(())
    Receptor-->>ClientSession: Ok(())
    ClientSession-->>RuntimeSession: Ok(())
    RuntimeSession-->>Runtime: Ok(())

2. persist — EU-closing mutation (snapshot_after=true)

Command with snapshot_after=true, disable_undo=false. In addition to the crash-recovery checkpoint, creates an experience_unit row and pushes the unit onto the undo stack.

sequenceDiagram
    participant Runtime
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    Runtime->>RuntimeSession: persist_success(tx_id, label, policy{snapshot_after=true})
    RuntimeSession->>ClientSession: persist(desc, disable_undo=false, snapshot_after=true, marker_id?)
    ClientSession->>Receptor: persist(context, ...)
    Receptor->>Store: persist(context, ...) [spawn_blocking]
    Store->>DB: snapshot = TransactionSnapshot::from_context
    Store->>DB: UPSERT recovery_session SET latest_checkpoint_id=<new>
    Store->>DB: INSERT OR REPLACE recovery_checkpoint (stack_pos=-1)
    note over Store,DB: redo_stack non-empty? DELETE EU+checkpoint redo rows, clear redo_stack_json
    Store->>DB: UPDATE recovery_checkpoint SET stack_pos=<undo top>
    Store->>DB: INSERT experience_unit (stack_kind='undo', stack_pos=<top>)
    Store->>DB: UPDATE recovery_session SET undo_stack_json, redo_stack_json, latest_checkpoint_id
    Store->>DB: COMMIT
    Store-->>Receptor: Ok(())
    Receptor-->>ClientSession: Ok(())
    ClientSession-->>RuntimeSession: Ok(())
    RuntimeSession-->>Runtime: Ok(())

3. persist — disable_undo mutation

Command with disable_undo=true. Writes the crash-recovery checkpoint, clears any pending redo stack, and permanently sets undo_checkpointing_enabled=0 for this transaction. No ExperienceUnit is created regardless of snapshot_after.

sequenceDiagram
    participant Runtime
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    Runtime->>RuntimeSession: persist_success(tx_id, label, policy{disable_undo=true})
    RuntimeSession->>ClientSession: persist(desc, disable_undo=true, snapshot_after=*, ...)
    ClientSession->>Receptor: persist(context, ...)
    Receptor->>Store: persist(context, ...) [spawn_blocking]
    Store->>DB: snapshot = TransactionSnapshot::from_context
    Store->>DB: UPSERT recovery_session SET latest_checkpoint_id=<new>
    Store->>DB: INSERT OR REPLACE recovery_checkpoint (stack_pos=-1)
    note over Store,DB: redo_stack non-empty? DELETE EU+checkpoint redo rows, clear redo_stack_json
    Store->>DB: UPDATE recovery_session SET undo_checkpointing_enabled=0
    Store->>DB: COMMIT
    note over Store: future persist calls skip EU creation
    Store-->>Receptor: Ok(())
    Receptor-->>ClientSession: Ok(())

4. undo_last — mid-stack (prior snapshot exists)

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: undo_last()
    ClientSession->>Receptor: can_undo(tx_id)
    Receptor->>Store: can_undo(tx_id)
    Store->>DB: load undo_stack_json
    Store-->>Receptor: true
    Receptor-->>ClientSession: true
    ClientSession->>Receptor: undo(tx_id)
    Receptor->>Store: undo(tx_id) [spawn_blocking]
    Store->>DB: load undo_stack + redo_stack
    Store->>DB: pop top EU → load prior EU's checkpoint snapshot
    Store->>DB: UPDATE experience_unit SET stack_kind='redo'
    Store->>DB: UPDATE recovery_checkpoint SET stack_kind='redo'
    Store->>DB: UPDATE recovery_session stacks + latest_checkpoint_id
    Store->>DB: COMMIT
    Store-->>Receptor: Ok(Some(snapshot))
    Receptor-->>ClientSession: Ok(Some(snapshot))
    ClientSession->>ClientSession: snapshot.restore_into(context)
    ClientSession-->>RuntimeSession: Ok(())

5. undo_last — last EU (restore to baseline)

Undoing the only remaining EU. The store pops it onto the redo stack and returns None because there is no prior snapshot.

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: undo_last()
    ClientSession->>Receptor: can_undo(tx_id)
    Receptor-->>ClientSession: true
    ClientSession->>Receptor: undo(tx_id)
    Receptor->>Store: undo(tx_id) [spawn_blocking]
    Store->>DB: pop last EU from undo_stack (stack now empty)
    Store->>DB: UPDATE EU → redo, UPDATE checkpoint → redo
    Store->>DB: UPDATE recovery_session stacks (undo=[], redo=[eu_id])
    Store->>DB: COMMIT
    Store-->>Receptor: Ok(None)  ← no prior snapshot = baseline
    Receptor-->>ClientSession: Ok(None)
    ClientSession->>ClientSession: import_staged_holons(HolonPool::new())
    ClientSession->>ClientSession: import_transient_holons(HolonPool::new())
    ClientSession-->>RuntimeSession: Ok(())

6. undo_last — nothing to undo (empty stack)

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: undo_last()
    ClientSession->>Receptor: can_undo(tx_id)
    Receptor->>Store: can_undo(tx_id)
    Store->>DB: load undo_stack_json → []
    Store-->>Receptor: false
    Receptor-->>ClientSession: false
    ClientSession-->>RuntimeSession: Err(Misc("No undo snapshot available..."))

7. redo_last

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: redo_last()
    ClientSession->>Receptor: redo(tx_id)
    Receptor->>Store: redo(tx_id) [spawn_blocking]
    Store->>DB: load undo_stack + redo_stack
    alt redo stack empty
        Store-->>Receptor: Ok(None)
        Receptor-->>ClientSession: Ok(None)
        ClientSession-->>RuntimeSession: Err(Misc("No redo snapshot available..."))
    else redo stack non-empty
        Store->>DB: pop top EU from redo → load its checkpoint snapshot
        Store->>DB: UPDATE EU → stack_kind='undo'
        Store->>DB: UPDATE checkpoint → stack_kind='undo'
        Store->>DB: UPDATE recovery_session stacks + latest_checkpoint_id
        Store->>DB: COMMIT
        Store-->>Receptor: Ok(Some(snapshot))
        Receptor-->>ClientSession: Ok(Some(snapshot))
        ClientSession->>ClientSession: snapshot.restore_into(context)
        ClientSession-->>RuntimeSession: Ok(())
    end

8. undo_to_marker

Pops all EUs from the top of the undo stack down to and including the marked EU, moving them all to the redo stack. Restores the snapshot of the EU just below the marker.

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: undo_to_marker(marker_id)
    ClientSession->>Receptor: undo_to_marker(tx_id, marker_id)
    Receptor->>Store: undo_to_marker(...) [spawn_blocking]
    Store->>DB: load EU stack (newest-first)
    alt marker_id not found
        Store-->>Receptor: Err(InvalidParameter)
        Receptor-->>ClientSession: Err(...)
        ClientSession-->>RuntimeSession: Err(...)
    else marker found
        note over Store: to_pop = EUs from top down to marker (inclusive)
        Store->>DB: for each popped EU: UPDATE EU → redo, UPDATE checkpoint → redo
        Store->>DB: UPDATE recovery_session stacks + latest_checkpoint_id
        Store->>DB: COMMIT
        alt marker was NOT the first EU
            Store-->>Receptor: Ok(Some(prior_snapshot))
            Receptor-->>ClientSession: Ok(Some(prior_snapshot))
            ClientSession->>ClientSession: snapshot.restore_into(context)
        else marker WAS the first EU (baseline)
            Store-->>Receptor: Ok(None)
            Receptor-->>ClientSession: Ok(None)
            ClientSession->>ClientSession: import_staged_holons(HolonPool::new())
            ClientSession->>ClientSession: import_transient_holons(HolonPool::new())
        end
        ClientSession-->>RuntimeSession: Ok(())
    end

9. redo_to_marker

Pops EUs from the redo stack up to and including the marked EU, restoring them to undo. Returns the snapshot of the marked EU (the state after it was applied).

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: redo_to_marker(marker_id)
    ClientSession->>Receptor: redo_to_marker(tx_id, marker_id)
    Receptor->>Store: redo_to_marker(...) [spawn_blocking]
    Store->>DB: load EU redo stack (newest-first on redo = oldest-first from undo perspective)
    alt marker_id not found on redo stack
        Store-->>Receptor: Err(InvalidParameter)
        Receptor-->>ClientSession: Err(...)
        ClientSession-->>RuntimeSession: Err(...)
    else marker found
        note over Store: to_restore = EUs from redo top down to marker (inclusive)
        Store->>DB: for each EU: UPDATE EU → undo, UPDATE checkpoint → undo
        Store->>DB: UPDATE recovery_session stacks + latest_checkpoint_id
        Store->>DB: COMMIT
        Store-->>Receptor: Ok(Some(marked_eu_snapshot))
        Receptor-->>ClientSession: Ok(Some(snapshot))
        ClientSession->>ClientSession: snapshot.restore_into(context)
        ClientSession-->>RuntimeSession: Ok(())
    end

10. recover_latest — crash recovery on startup

Called from ClientSession::recover when reopening a transaction after process restart.

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: ClientSession::recover(space_manager, recovery, tx_id)
    ClientSession->>ClientSession: open_transaction_with_id(tx_id)
    ClientSession->>ClientSession: restore_from_recovery()
    ClientSession->>Receptor: recover_latest(tx_id)
    Receptor->>Store: recover_latest(tx_id)
    Store->>DB: SELECT latest_checkpoint_id FROM recovery_session WHERE tx_id=?
    alt no session row
        Store-->>Receptor: Ok(None)
        Receptor-->>ClientSession: Ok(None)
        note over ClientSession: context left at empty open state
    else checkpoint found
        Store->>DB: SELECT snapshot_blob FROM recovery_checkpoint WHERE checkpoint_id=?
        Store->>Store: snapshot.verify_integrity()
        Store-->>Receptor: Ok(Some(snapshot))
        Receptor-->>ClientSession: Ok(Some(snapshot))
        ClientSession->>ClientSession: snapshot.restore_into(context)
    end
    ClientSession-->>RuntimeSession: Ok(Self)

11. cleanup — on commit

Called by RuntimeSession::commit_transaction after context.commit() succeeds. Removes all recovery state for the transaction (CASCADE deletes checkpoints + EUs).

sequenceDiagram
    participant RuntimeSession
    participant ClientSession
    participant Receptor
    participant Store
    participant DB

    RuntimeSession->>ClientSession: cleanup()
    ClientSession->>Receptor: cleanup(tx_id)
    Receptor->>Store: cleanup(tx_id) [spawn_blocking]
    Store->>DB: DELETE FROM recovery_session WHERE tx_id=?
    note over DB: ON DELETE CASCADE removes recovery_checkpoint + experience_unit rows
    Store->>DB: COMMIT
    Store-->>Receptor: Ok(())
    Receptor-->>ClientSession: Ok(())
    ClientSession-->>RuntimeSession: Ok(())
    RuntimeSession->>RuntimeSession: archive_transaction(tx_id)

12. list_open_sessions — startup recovery scan

Called by RuntimeSession::restore_open_sessions at process startup to find transactions that were open when the process last crashed.

sequenceDiagram
    participant Host
    participant RuntimeSession
    participant Receptor
    participant Store
    participant DB

    Host->>RuntimeSession: restore_open_sessions()
    RuntimeSession->>Receptor: list_open_sessions()
    Receptor->>Store: list_open_sessions()
    Store->>DB: SELECT tx_id FROM recovery_session WHERE lifecycle_state='Open'
    Store-->>Receptor: Ok(Vec<tx_id_str>)
    Receptor-->>RuntimeSession: Ok(Vec<tx_id_str>)
    loop for each tx_id
        RuntimeSession->>RuntimeSession: ClientSession::recover(space_manager, recovery, tx_id)
        note over RuntimeSession: recover_latest called inside — restores to last checkpoint
        RuntimeSession->>RuntimeSession: register_recovered_session(session)
    end
    RuntimeSession-->>Host: Ok(restored_count)

13. can_undo / can_redo — stack inspection (UI query)

Lightweight reads used to determine whether undo/redo controls should be enabled. No write to DB.

sequenceDiagram
    participant Caller
    participant Receptor
    participant Store
    participant DB

    Caller->>Receptor: can_undo(tx_id)
    Receptor->>Store: can_undo(tx_id)
    Store->>DB: SELECT undo_stack_json FROM recovery_session WHERE tx_id=?
    Store-->>Receptor: Ok(undo_stack.len() > 0)
    Receptor-->>Caller: Result<bool>

    Caller->>Receptor: can_redo(tx_id)
    Receptor->>Store: can_redo(tx_id)
    Store->>DB: SELECT redo_stack_json FROM recovery_session WHERE tx_id=?
    Store-->>Receptor: Ok(redo_stack.len() > 0)
    Receptor-->>Caller: Result<bool>

14. undo_history — undo stack label list (UI query)

Returns the description strings of all closed EUs in undo order (oldest first).

sequenceDiagram
    participant Caller
    participant Receptor
    participant Store
    participant DB

    Caller->>Receptor: list_undo_history(tx_id)
    Receptor->>Store: undo_history(tx_id) [spawn_blocking]
    Store->>DB: SELECT description FROM recovery_checkpoint WHERE tx_id=? AND stack_kind='undo' AND stack_pos >= 0 ORDER BY stack_pos ASC
    Store-->>Receptor: Ok(Vec<String>)
    Receptor-->>Caller: Ok(Vec<String>)

DB schema summary (for reference)

recovery_session
  tx_id PK, lifecycle_state, latest_checkpoint_id FK,
  undo_stack_json, redo_stack_json,
  undo_checkpointing_enabled, format_version, updated_at_ms

recovery_checkpoint
  checkpoint_id PK, tx_id FK→session(CASCADE),
  stack_kind ('undo'|'redo'), stack_pos (-1=sentinel, ≥0=EU-linked),
  snapshot_blob, snapshot_hash, description, disable_undo, created_at_ms

experience_unit
  unit_id PK, tx_id FK→session(CASCADE),
  marker_id?, marker_label?,
  checkpoint_id FK→recovery_checkpoint,
  stack_kind ('undo'|'redo'), stack_pos, created_at_ms