Skip to content

History-derived undo plans#175

Open
marcus-pousette wants to merge 4 commits into
cybersemics:mainfrom
marcus-pousette:codex/lazy-undo-history
Open

History-derived undo plans#175
marcus-pousette wants to merge 4 commits into
cybersemics:mainfrom
marcus-pousette:codex/lazy-undo-history

Conversation

@marcus-pousette

@marcus-pousette marcus-pousette commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds a TreeCRDT edit API for app-level undo/redo.

  • edits.capture(...) records the local operations produced by one user edit.
  • edits.undo(...) turns a captured edit into new local TreeCRDT operations that reverse the visible effect.
  • edits.redo(...) reapplies an undone edit and returns a fresh undo plan.
  • The playground now has Undo/Redo buttons wired through this API.

Undo/redo stacks are still owned by the app. TreeCRDT provides the operations and plans; the app decides what counts as one user edit, how many edits to keep, when to clear redo, and whether to persist the stack.

How Undo/Redo Works

Undo does not delete or rewrite old operations. It is closer to git revert: TreeCRDT looks at the state around the captured edit, creates a new forward local edit that restores the previous visible state, and appends that new edit to the oplog.

Redo works the same way in the other direction. Undo returns a redo plan, redo returns a new undo plan, so chains like undo/undo/redo/redo keep working as normal local writes.

Because undo and redo are normal TreeCRDT writes, existing sync, merge behavior, materialization events, and write ids continue to work.

Cost Model

A naive undo implementation would capture inverse metadata during every normal write. That means inserts, moves, deletes, and payload writes may need extra reads of parents, siblings, or previous payloads even when the user never presses undo.

This PR keeps the normal write path lighter: edits.capture(...) only records the operations produced by the edit. The more expensive work is deferred until undo time.

For SQLite-backed engines, history inversion first tries a suffix rewind from the captured edit frontier inside a rollbackable savepoint. For recent edits, this is bounded by the number of operations since that edit, not the full oplog. If the fast path cannot prove the case safely, it falls back to canonical replay from history, which is slower but conservative.

So the tradeoff is: cheap common writes, potentially more work on rare undo, with a correctness-first fallback.

Implementation Support

Implementation Status
@treecrdt/sqlite-node Supported. Exposes engine.history.invert through the SQLite extension path.
@treecrdt/wa-sqlite Supported. Uses the same SQLite-backed history inversion path in browser/worker runtimes.
@treecrdt/postgres-napi Lazy history inversion is not implemented yet. edits.undo(...) on a lazy captured edit rejects because the engine does not expose engine.history.
Interface-only eager plans Still supported through edits.capturePlan(...), which captures undo metadata immediately on the write path.

The conformance suite covers eager undo/redo everywhere it applies, history-derived undo/redo for engines that expose engine.history, repeated undo/redo chains, new writes after undo, imported operations between capture and undo, and materialization event/writeId behavior.

@marcus-pousette marcus-pousette force-pushed the codex/lazy-undo-history branch 13 times, most recently from 0e02045 to e87f392 Compare June 22, 2026 12:07
@marcus-pousette marcus-pousette force-pushed the codex/lazy-undo-history branch from e87f392 to 44c71e7 Compare June 22, 2026 12:28
@marcus-pousette marcus-pousette force-pushed the codex/lazy-undo-history branch from 051a0d6 to 1e570cb Compare June 23, 2026 09:15
@marcus-pousette marcus-pousette marked this pull request as ready for review June 24, 2026 20:17
@marcus-pousette marcus-pousette changed the title Explore history-derived undo plans History-derived undo plans Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant