ADR 0001: Undo/Redo scoped to local changes in worker sync
Date: 2026-01-28
Status: Accepted
Context
The worker-based db-sync flow applies remote updates directly to the local DB.
Undo/redo must remain usable for the current user without replaying or reverting
server changes. The system already supports tx-meta flags (e.g., :local-tx?,
:gen-undo-ops?, :undo?, :redo?) and has conflict checks in undo logic.
We must ensure undo/redo never attempts a transaction that would result in
invalid client or server data. We also want a simple, sane model that avoids
tracking per-remote update history.
Constraints and goals:
- No server echo of the user's own txs back to the same client.
- Best effort undo: if conflicts make an undo unsafe, skip it and keep data
valid (never force invalid data).
- Remote updates must not create undo history.
- Local updates must create undo history as before.
Decision
1) Only local txs may generate undo/redo ops. A tx is local if and only if
tx-meta contains :local-tx? true.
2) All remote/apply/rebase/import paths must set :local-tx? false and
:gen-undo-ops? false.
3) Undo/redo will be "best effort": if reversing a tx would violate invariants
(e.g., moved block, deleted parent, remaining children), the undo op is
dropped and history is cleared for that repo to prevent invalid data.
This keeps undo/redo stable and ensures remote updates never appear in the
user's undo stack.
Architecture
Source of truth for origin
- :local-tx? is the sole origin flag.
- Local changes (UI/outliner ops) attach :local-tx? true at the time of
submission to the worker.
- Remote changes (db-sync apply-remote, rtc remote update, snapshot import) set
:local-tx? false and :gen-undo-ops? false.
Undo/redo recording
- Undo history is recorded only when tx-meta has :local-tx? true.
- Additional existing gates remain: :outliner-op is present, :gen-undo-ops?
is not false, and :create-today-journal? is not true.
- Redo stack is cleared on any new local undo-recorded op (existing behavior).
Undo/redo execution safety
- Reverse datoms are computed from original tx-data.
- Conflict detection remains in place (moved blocks, deleted parents, children
still exist). These are treated as safe failures for best-effort undo.
- If a conflict is detected or reverse-tx is empty, the undo op is dropped and
history cleared for that repo to avoid invalid transacts.
- Undo/redo execution uses tx-meta flags :undo?/:redo? and :gen-undo-ops? false
to avoid recursive undo generation.
Consequences
- Undo/redo only affects user-originated changes, never server updates.
- In rare cases, undo may be skipped after a remote conflict, but the database
remains valid.
- The model stays simple: no remote history tracking or per-entity versioning.
Plan
1) Confirm the origin flag path
- Verify all local transact entry points set :local-tx? true
(e.g., frontend.db.transact/transact, apply-outliner-ops).
- Verify all remote/apply/rebase/import paths set :local-tx? false and
:gen-undo-ops? false (db-sync apply-remote, rtc remote updates,
snapshot import, db restore).
2) Gate undo generation on :local-tx?
- Update frontend.undo-redo/gen-undo-ops! to require :local-tx? true in
tx-meta in addition to existing checks.
3) Enforce best-effort safety
- On conflict (moved block, deleted parent, remaining children) or on
missing reverse tx data, drop the undo op and clear history for repo.
4) Documentation
- Update relevant internal docs or comments near undo/redo about the origin
flag and best-effort semantics.
Test Scenarios (Manual)
1) Local change only
- Create a block, undo, redo.
- Expected: undo/redo works and affects only local changes.
2) Remote change only
- Receive a server update that modifies a block.
- Expected: undo stack is unchanged; undo does not revert remote update.
3) Local change after remote update
- Remote updates a block; user edits same block; undo.
- Expected: undo reverts only the user's edit; remote update remains.
4) Remote delete of parent before undo
- User creates child; remote deletes parent; user tries undo.
- Expected: undo is skipped safely; history cleared; no invalid data.
5) Remote move before undo
- User moves block; remote moves or deletes target; user tries undo.
- Expected: undo is skipped safely; history cleared; no invalid data.
6) Mixed local ops and remote tx batch
- Interleave local edits and remote sync.
- Expected: only local edits appear in undo stack; undo never produces
invalid data.
Open Questions
- Do we need a user-visible notification when undo is skipped due to conflicts,
or is silent failure acceptable?
Silent failure acceptable.