Beide Recherchen zur Map #16 lagen nur auf lokalen Wegwerf-Branches. Die Kurzfassungen stehen an den Tickets #21 und #22, die Langfassungen sind mit Quellen und der Trennung zwischen Belegtem, Abgeleitetem und Unbelegtem aber das, worauf man zurueckkommt. Beide enthalten Messungen, die keine Dokumentation hergibt: dass trash 5.2.6 auf Netzlaufwerken gar nicht funktioniert und Windows dort sonst still und endgueltig loeschen wuerde, und dass ein ungeprunter Append-Log im Yjs-Benchmark das Siebzigfache des Rohtexts misst. Ausserdem /.claude in die .gitignore - dort liegen lokale Einstellungen und die temporaeren Worktrees der Recherche-Agenten.
67 KiB
Was es kostet, Historie später möglich zu halten
Research zu Gitea-Issue #22. Stand: 2026-08-28. Recherchiert gegen Primärquellen (Hersteller-Doku, Quellcode auf GitHub, Microsoft Learn, docs.rs, crates.io). Nichts davon wurde gebaut oder kompiliert. Es wurde kein Anwendungscode geschrieben und keine Dependency hinzugefügt. Alles unter „Belegt" stammt aus der jeweils genannten Quelle; alles Abgeleitete ist als solches markiert.
Historie ist nicht geplant. Dieses Dokument beschafft nur die Fakten für die Schreib-Entscheidung in #19.
Kurzantwort
Nein — ein Schreibverfahren „temporäre Datei schreiben, dann umbenennen" schließt eine spätere Historie nicht aus. Es ist im Gegenteil das einzige der beiden zur Wahl stehenden Verfahren, das Historie überhaupt billig macht.
Die Begründung in einem Satz: Historie braucht die Vorfassung, und beim Umbenennen liegt die Vorfassung
zum Zeitpunkt des Schreibens noch unversehrt auf der Platte — ein direktes fs::write hat sie da schon
vernichtet.
Der Einwand aus der Beratungsrunde trifft eine reale Gefahr, aber am falschen Ort. Teuer wird Historie nicht durch das Wie des Schreibens, sondern durch das Wo: Wenn nach einem halben Jahr fünf Stellen im Code eine Notiz auf die Platte schreiben, muss man fünf Stellen anfassen. Wenn es genau eine gibt, ist Historie eine Änderung innerhalb dieser einen Funktion — und alles Weitere (Format, Ablageort, Aufbewahrung) eine Entscheidung, die man dann trifft, nicht heute.
Der Preis dafür, Historie möglich zu halten, beträgt also: eine Funktion. Keine Datei, kein Format, kein Byte auf der Platte, keine Dependency.
Und die Asymmetrie läuft genau andersherum als behauptet: Nicht das schlichte Schreiben ist die schwer umkehrbare Entscheidung, sondern der Append-Log. Von „Datei ist die Wahrheit" zu „Datei plus Sicherungen" ist eine Ergänzung. Von „Log ist die Wahrheit" zurück ist eine Migration. Wer die Option offenhalten will, hält sie gerade dadurch offen, dass er den Log nicht baut.
Dass der Log die Einbahnstraße ist, ist dabei nicht nur ein Architektur-Argument: Die Automerge-Maintainer schreiben selbst, dass sich Historie nicht verwerfen lässt, ohne das Dokument zu ersetzen, und Yjs begründet in seiner README, warum Tombstones konstruktionsbedingt liegenbleiben müssen (3.2).
1. Was Historie technisch überhaupt braucht
Bevor man Kosten schätzt, lohnt es, die Anforderung auf ihre Bestandteile zu zerlegen. Eine Historie — gleich welcher Bauart — braucht genau vier Dinge:
- Einen Auslöser. Ein Zeitpunkt, an dem entschieden wird: jetzt entsteht eine neue Fassung.
- Die Vorfassung. Der Inhalt, der gleich ersetzt wird.
- Einen Ablageort. Irgendwo müssen die alten Fassungen hin.
- Eine Aufräumregel. Ohne sie wächst der Ablageort unbegrenzt.
Nur Punkt 1 und 2 hängen am Schreibverfahren. Punkt 3 und 4 sind reine Folgeentscheidungen und komplett
unabhängig davon, wie die .md auf die Platte kommt.
Und beide, Auslöser und Vorfassung, fallen beim temp+rename-Verfahren ohnehin an:
- Der Auslöser ist der Aufruf der Schreibfunktion. Präziser geht es nicht.
- Die Vorfassung liegt in dem Moment, in dem die temporäre Datei fertig geschrieben ist und das Umbenennen ansteht, noch vollständig und unangetastet unter ihrem Pfad. Man muss sie nur lesen, bevor man umbenennt.
Bei einem direkten fs::write auf die Zieldatei ist die Vorfassung dagegen nach dem ersten Byte weg. Wer dort
später Historie nachrüsten will, muss die Datei zusätzlich vorher lesen — was möglich, aber ein
Extra-Schritt ist, den das rename-Verfahren geschenkt bekommt.
Nebenbefund, der #19 unabhängig von Historie betrifft: #20 (Zeitstempel-Konflikt) verlangt ohnehin, dass die Schreibfunktion vor dem Schreiben den Zustand der Zieldatei prüft. Wer dort statt eines
statgleich den Inhalt liest, hat die Vorfassung ohne zusätzlichen Syscall in der Hand. Das ist kein Argument, es heute zu tun — nur der Beleg, dass es später auch nichts kostet.
2. Wie vergleichbare lokale Markdown-Editoren es lösen
Kurzüberblick, Details und Belege in den Unterabschnitten:
| Werkzeug | Wo liegt die Historie? | Format | Intervall | Aufbewahrung | Neben den .md? |
|---|---|---|---|---|---|
| Obsidian (File recovery) | App-Systemordner, %APPDATA%\Obsidian\ — außerhalb des Vault |
Volltext-Snapshots | 5 Min (Default) | 7 Tage (Default) | Nein |
| Obsidian Sync (Version history) | Auf Obsidians Servern | serverseitig | — | 1 Monat (Standard) / 12 Monate (Plus) | Nein, kostenpflichtig |
| Joplin (Note History) | Tabelle revisions in database.sqlite |
Diff-Kette (diff-match-patch, JSON) |
10 Min | 90 Tage (Default) | Nein — es gibt gar keine .md |
| Logseq (0.10.9) | logseq/bak/<datei>/ im Graph-Ordner |
Volltext-Kopien, Zeitstempel im Namen | nur bei Konflikt oder Schreibfehler, nicht bei jedem Speichern | 6 Fassungen pro Datei | Ja |
| Zettlr | — es gibt keine | — | — | — | — |
| Syncthing (kein Editor, aber das beste Vorbild) | .stversions/ im Sync-Ordner |
Volltext-Kopien mit Zeitstempel im Namen | pro Änderung | konfigurierbar, logarithmisch ausgedünnt | Ja |
Das auffälligste Muster: Von den untersuchten Editoren legt keiner seine Historie neben die
Notizdateien. Sie liegt entweder im App-Ordner (Obsidian), in einer Datenbank (Joplin) oder auf einem Server
(Obsidian Sync). Wer neben den .md versioniert, ist ein Sync-Werkzeug, kein Editor.
2.1 Obsidian — Historie als aufgesetztes Core-Plugin, komplett außerhalb des Vault
Obsidian ist der direkteste Vergleichspunkt: lose .md-Dateien in einem Ordner, dasselbe Versprechen an den
Nutzer, dieselbe Ausgangslage.
Belegt (Zitate aus der offiziellen Help-Seite https://obsidian.md/help/plugins/file-recovery):
- „File recovery is a core plugin that protects your work from accidental deletions, file corruption, or unwanted changes by automatically saving complete snapshots of your notes at regular intervals."
- „Snapshots capture the full content of your files, not just changes, allowing you to restore any previous version." — also Volltext, keine Diffs.
- „By default, snapshots are saved a minimum of 5 minutes from each other, and kept for 7 days."
- „Snapshots are kept in the Global settings, outside of the vault, to account for vault-related data loss. This means that snapshots are stored with the absolute path to the note. If you've moved your vault recently, you may need to move it back to the location where it was when the snapshot was taken."
- „If you are using Obsidian Sync or other syncing services, File recovery snapshots will not sync between devices. Snapshots are device-specific and remain local to each device."
- „File recovery is not a complete backup solution."
- Nur
.mdund.canvassind wiederherstellbar. - Aufräumen automatisch nach Ablauf der Tagesgrenze, manuell über Clear history.
Der Ablageort ist damit belegt als der Electron-userData-Ordner der App —
Windows: %APPDATA%\Obsidian\ (https://obsidian.md/help/data-storage), nicht .obsidian/ im Vault
und nicht neben den Notizen.
Vermutung (starkes Indiz, nicht wörtlich belegt): Die Snapshots liegen in Obsidians IndexedDB/LevelDB. Die Doku nennt bei IndexedDB nur Sync-State und Metadata-Cache, führt aber für File recovery dieselbe Apple-Lockdown-Mode-Einschränkung auf wie für IndexedDB. Ein Serialisierungsformat ist nirgends dokumentiert; Obsidian ist Closed Source.
Widerspruch in der Doku selbst, den man kennen sollte: Der Abschnitt „Storage and performance" derselben Seite behauptet, Snapshots „only store changed files", während der Haupttext „full content […] not just changes" sagt. Plausible Auflösung (Vermutung): Nur geänderte Dateien werden überhaupt gesnapshottet, jede davon aber als Volltext.
Zum Schreibverfahren: Es gibt keine offizielle Aussage, ob Obsidian atomar (temp+rename) oder
in-place schreibt. Belegt ist nur die Plugin-API-Semantik: Vault.process ist dokumentiert als „Atomically
read, modify, and save the contents of a note" und garantiert, „that the file doesn't change between
reading the current content and writing the updated content"
(https://docs.obsidian.md/Plugins/Vault). Das ist eine Sperre gegen konkurrierende Plugins innerhalb des
Obsidian-Prozesses, keine dokumentierte Aussage über rename()-Atomarität gegenüber dem Dateisystem —
diese Unterscheidung macht die Doku nirgends explizit (Lesart, nicht wörtlich belegt). Der Changelog zu
1.13.4 (30.07.2026) enthält den Eintrag „Fixed a rare race condition where a file could get stuck and fail to
save until the app was restarted" (https://obsidian.md/changelog/2026-07-30-desktop-v1.13.4/) — ein Beleg
dafür, dass der Save-Pfad auch nach über zehn Jahren nicht trivial ist.
Warum das für #22 der wichtigste Befund ist: Obsidians Historie ist ein nachträglich aufgesetztes Plugin, das mit dem Schreibverfahren des Kerns nichts zu tun hat. Es beobachtet Dateien und legt Kopien woanders ab. Genau das ist das „Historie ist eine Schicht über dem Schreiben"-Modell — von dem am längsten etablierten Werkzeug dieser Kategorie in Produktion vorgeführt.
Der Preis, den Obsidian für diesen Ort zahlt (alles wörtlich aus der Doku oben): absolute Pfade, die nach einem Vault-Umzug brechen; keine Mitnahme beim Backup; kein Sync zwischen Geräten. Ein Mittelweg — Historie in einem versteckten Ordner im Vault — hat von den untersuchten Werkzeugen keiner gewählt; für Obsidian existiert dazu ein offener Feature-Request (https://forum.obsidian.md/t/file-recovery-store-file-snapshots-in-the-vault-folder-instead-of-the-obsidians-system-folder/116677, Sekundärquelle, nur als Hinweis).
2.2 Joplin — der Gegenentwurf: keine .md, dafür Diff-Ketten in SQLite
Joplin ist als Vergleich nur begrenzt tauglich, weil es das Versprechen, das DioNotes gibt, gar nicht erst macht — aber es zeigt sehr genau, was man mit einer Datenbank dahinter gewinnt.
Belegt (https://joplinapp.org/help/dev/spec/user_profile/): database.sqlite ist „The central SQLite
database file where all notes, notebooks, tags, and metadata are stored". Es gibt keine .md-Dateien.
Unter Windows: %UserProfile%\.config\joplin-desktop.
Belegt (https://joplinapp.org/help/apps/note_history/):
- „All the apps save a version of the modified notes every 10 minutes."
- Aufbewahrung „by default, for 90 days".
- „These revisions are then synced across all the devices." — anders als bei Obsidian.
- Wiederherstellen kopiert in einen Ordner „Restored Notes", ohne die aktuelle Version zu verändern.
- Bei unterschiedlichen Einstellungen pro Gerät gewinnt die kürzeste Aufbewahrungsdauer.
Belegt aus dem Quellcode (https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Revision.ts, https://github.com/laurent22/joplin/blob/dev/packages/lib/services/RevisionService.ts, https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts):
-
Tabelle
revisions, Felder u. a.title_diff,body_diff,metadata_diff,item_id,parent_id. -
Diffs über Google
diff-match-patch, serialisiert als JSON (createTextPatch→JSON.stringify(dmp.patch_make(old, new))). -
Defaults:
revisionService.ttlDays= 90,revisionService.intervalBetweenRevisions= 10 Min (nicht in der UI sichtbar),revisionService.oldNoteInterval= 7 Tage. -
Die erste Revision einer Notiz ist ein Volltext-Patch gegen den Leerstring; Folgerevisionen sind Diffs mit
parent_idauf die Vorgängerin.mergeDiffs()läuft die Kette bis zur Wurzel zurück. -
Aufräumen mit Rebase am Kopf — der interessanteste Teil. Kommentar wörtlich aus
Revision.ts:„When deleting old revisions, we need to make sure that the oldest surviving revision is a 'merged' one (as opposed to a diff from a now deleted revision). So every time we deleted a revision, we need to find if there's a corresponding surviving revision and modify that revision into a 'merged' one."
Konkret: Die älteste überlebende Revision wird rekonstruiert und als Volltext-Patch gegen den Leerstring neu geschrieben,
parent_idauf''. Damit bleibt die Kettenlänge auf das TTL-Fenster begrenzt. -
Änderungen, die aus Sync oder Entschlüsselung stammen, werden ausgefiltert — nur echte lokale Edits erzeugen Revisionen.
Die Lehre für DioNotes: Diff-Ketten sind sparsam, aber nicht umsonst. Sie kosten eine Rekonstruktion in
O(Kettenlänge) bei jedem Anlegen und jedem Anzeigen einer Revision, plus eine nicht triviale
Rebase-Logik beim Aufräumen, plus einen dokumentierten Fehlerpfad, wenn eine zu löschende Revision noch
verschlüsselt ist. Obsidian hat für dasselbe Feature Volltext gewählt und dafür die Aufbewahrung auf 7 statt
90 Tage gesetzt. Retention-Dauer und Speicherstrategie hängen direkt zusammen — das ist die eigentliche
Stellschraube, und sie liegt vollständig in der Zukunft.
2.3 Logseq — Sicherungskopien im Workspace, aber nur wenn es brenzlig wird
Logseq ist für DioNotes der lehrreichste Fall, weil es als einziges untersuchtes Werkzeug seine Sicherungskopien neben die Notizen legt — genau der Mittelweg aus der Ticket-Frage — und weil dabei sichtbar wird, wo dieser Weg scheuert.
Versionsvorbehalt: Der
master-Branch von Logseq ist inzwischen die DB-basierte Neuentwicklung ohnehandler/file.cljs. Alle Datei-relevanten Belege stammen deshalb aus Tag0.10.9, der letzten dateibasierten Release-Linie.
Belegt — drei generierte Verzeichnisse im Graph-Ordner:
| Pfad | Zweck | Beleg |
|---|---|---|
logseq/bak |
Sicherung überschriebener Dateien | backup_file.cljs#L7 |
logseq/version-files/local |
explizit angeforderte Versionen | backup_file.cljs#L8 |
logseq/.recycle |
Papierkorb — Löschen ist ein renameSync dorthin |
handler.cljs#L55-L67 |
Namenskonvention — pro Datei ein eigener Unterordner, Zeitstempel im Dateinamen
(backup_file.cljs#L10-L51):
<graph>/logseq/bak/pages/Foo/2024-01-15T10_23_45.678Z.Desktop.md
└ relativer Pfad ┘└ ISO-8601, ":" → "_" ┘└ Plattform ┘
Der wichtigste Befund: Es wird NICHT bei jedem Speichern gesichert. Es gibt genau drei Auslöser (belegt):
- Erkannte Abweichung zwischen Platte und DB beim Schreiben
(
fs/node.cljs#L54-L57) — und selbst dann filtert der Electron-Handler ein zweites Mal und sichert nur, wenn der Diff eine Löschung zeigt (handler.cljs#L75-L80):(when (and (string? db-content) (string? new-content) (string-some-deleted? db-content new-content)) (backup-file/backup-file repo :backup-dir path ...)) - Schreibfehler (EBUSY, Lock, fehlende Rechte) — mit Notification „A backup file was saved to …"
(
handler.cljs#L117-L140). - Externe Dateiänderung über den Watcher, bevor sie in die DB übernommen wird — Quellcode-Kommentar
wörtlich: „save the previous content in a versioned bak file to avoid data overwritten."
(
watcher_handler.cljs#L44-L56).
Maintainer cnrpman fasst es zusammen: „Basically it writes to bak when there's a dangerous file
operation… It's a safe net." (Issue #3370).
Das ist keine Historie, sondern ein Fangnetz — und genau das macht es für DioNotes interessant: Der billigste denkbare Nutzen (Datenverlust bei Konflikten verhindern) wird mit dem billigsten denkbaren Mechanismus erkauft, ohne dass irgendetwas am Schreibverfahren hängt.
Aufräumen — es gibt eine Grenze, sie greift nur zu lokal (belegt,
backup_file.cljs#L27-L34):
(defn- truncate-old-versioned-files!
"reserve the latest 6 version files"
[dir]
... (drop 6 (reverse (sort files))) ... removeSync ...)
Warum der Ordner trotzdem wächst (Ableitung aus dem Code, deckt sich mit den Nutzerberichten):
- Die Grenze gilt pro Datei-Unterordner; die Zahl der Unterordner ist unbegrenzt.
- Aufgeräumt wird nur beim Schreiben eines neuen Backups in genau diesen Ordner. Kein Startup-Sweep, kein Hintergrundjob. Ordner von Dateien, die nie wieder konfligieren, bleiben ewig stehen.
- Sortiert wird rein lexikografisch — funktioniert nur, weil ISO-8601 lexikografisch = chronologisch ist.
Die Retention-Zahl wanderte über die Jahre: nie getrunct (≤ 0.6.1) → 10
(PR #4421, Commit-Text: „Files in the logseq/bak folder are
never truncated") → 3 → 6. Gründer tiensonqin empfiehlt in
Issue #4819: „I suggest to cleanup backups using some
cron scripts." Es gibt keine offizielle Doku zum bak-Ordner und keinen Off-Schalter
(Forum, Aug 2025,
unbeantwortet).
Korrektur einer verbreiteten Annahme: Beschwerden über Wachstum gibt es reichlich (#4819, #3588 „600 or more backup files", #3370 offen seit 2021, Discussion #8624 seit drei Jahren unbeantwortet). Aber keine Primärquelle nennt eine konkrete GB-Zahl. Belegt ist unbegrenztes Ordner-Wachstum, nicht unbegrenztes Größenwachstum.
Weitere belegte Details:
- Nicht atomar geschrieben:
fs/writeFileSync path content, mit vorherigemchmodSyncauf644, falls die Datei nicht schreibbar ist (handler.cljs#L117-L126). - Watcher: chokidar,
awaitWriteFinish true(fs_watcher.cljs). - Konflikt: Vergleich vor dem Schreiben; bei Abweichung wird nicht geschrieben, sondern ein Modal
gezeigt mit zwei editierbaren Textareas („On disk:" / „In Logseq:"), je einem Button „Select this" —
kein Merge, kein Abbrechen
(
components/diff.cljs#L27-L90). - Undo überlebt den Neustart nicht. Der Stack ist ein reines In-Memory-Atom über
DataScript-Datoms —
(def ^:private undo-redo-states (atom {}))(undo_redo.cljs#L16-L28). Die offizielle Doku formuliert es vorsichtig: „during their editing sessions" (docs: Undo and Redo). - Es gibt keine UI, die bak-Versionen anzeigt oder wiederherstellt — nur „Open page backups directory",
das den Explorer öffnet. Der Menüpunkt „View page history" ist dagegen Git-basiert
(
shell/get-file-latest-git-log) und setzt ein eigenes Git-Setup des Nutzers voraus (page_menu.cljs#L93-L159).
Die Lehre: Selbst das Werkzeug, das im Workspace sichert, hat sich gegen „bei jedem Speichern" und für „nur wenn es brenzlig wird" entschieden — und hat trotzdem ein Aufräumproblem, weil die Grenze pro Datei statt global gilt. Beides sind Entwurfsfehler, die man beim Nachrüsten vermeiden kann, weil sie mit dem Schreibverfahren nichts zu tun haben.
2.4 Zettlr — gar keine Historie, und das ist die offizielle Position
Belegt: Zettlr hat kein Snapshot-, History- oder Versions-Feature. Die Doku delegiert das Thema ausdrücklich an den Nutzer (https://docs.zettlr.com/en/first-time-users/workspaces.html):
„We strongly recommend that you regularly back up your workspaces. The simplest method is to use a cloud storage provider (such as Dropbox, Google Drive, OneDrive, or iCloud)… you should also back up the files onto an external storage device from time to time."
Vorhanden ist nur der System-Papierkorb via shell.trashItem
(safe-delete.ts)
und ein überschreibendes Autosave.
Schreibverfahren — nicht atomar (belegt,
fsal/index.ts#L529-L533):
public async writeTextFile (filePath: string, contents: string): Promise<void> {
await this._cache.del(filePath)
await fs.writeFile(filePath, contents, 'utf-8')
}
Drei Details des Save-Pfads, die #18 bis #20 direkt betreffen (belegt,
documents/index.ts#L1527-L1603):
- Der Descriptor wird vor dem Schreiben aktualisiert, mit explizitem Kommentar zur Race Condition „User ändert → Save startet → User ändert mehr → Save beendet und macht die Änderungen rückgängig".
- Self-Echo-Unterdrückung:
this._ignoreChanges.push(filePath)— der eigene Write wird beim Watcher-Event wieder herausgefiltert. - BOM und Zeilenende werden rekonstruiert: intern arbeitet Zettlr immer mit
\n, beim Schreiben wird der ursprüngliche Zustand wiederhergestellt (doc.descriptor.bom + content.split('\n').join(doc.descriptor.linefeed)).
Konfliktbehandlung, dreistufig (belegt,
documents/index.ts#L1070-L1130) —
für #20 unmittelbar verwertbar:
- mtime-Filter, bewusst
modtime <= ourModtime → returnstatt Gleichheitsprüfung. Kommentar wörtlich: „In response to issue #1621: We will not check for equal modtime but only for newer modtime to prevent sluggish cloud synchronization services (e.g. OneDrive and Box) from having text appear to 'jump'." - Inhaltsvergleich:
if (diskContents === doc.lastSavedContent) return— fängt Cloud-Dienste ab, die nur Attribute anfassen. - Dialog „File changed on disk" mit den Buttons „Keep editor contents" / „Load changes from disk"
(kein Merge, kein Diff) und einer Checkbox „Always load changes from disk if there are no unsaved changes
in the editor", die direkt die Einstellung
alwaysReloadFilesschreibt.
Zur Git-Frage: Es gibt kein offizielles Zettlr-Git-Plugin (belegt). Zettlr/Zettlr-Git-Plugin
liefert 404; die Organisation hat keine Plugin-Repos
(https://github.com/orgs/Zettlr/repositories). Zettlr hat überhaupt kein Plugin-System — Maintainer
nathanlesage: „Writing a plugin system is incredibly difficult, and thus we should do that extremely
limited." (Discussion #3108). Das einzige Git-Feature
ist reine Erkennung: dir.isGitRepository = (await fs.lstat(path.join(dir.path, '.git'))).isDirectory()
(fsal-directory.ts#L103-L105).
Issue #1050 „Integrate basic git functionality" ist seit Juli
2020 offen, Label „Priority: Low".
Die Lehre: Ein etablierter, ernstgemeinter Markdown-Editor kommt seit Jahren ohne jede eingebaute Historie aus und verweist stattdessen auf externe Werkzeuge. Das ist der empirische Beleg dafür, dass „Historie nicht bauen" eine tragfähige Produktentscheidung ist — nicht bloß ein Aufschub.
2.5 Syncthing — kein Editor, aber das beste Vorbild für Ablage und Aufräumen
Syncthing ist der einzige untersuchte Vertreter, der Versionen neben den Nutzdateien ablegt, und hat dafür die durchdachteste Aufräumstrategie. Alles hier belegt aus https://docs.syncthing.net/users/versioning.html:
-
Ablageort: Standardmäßig der Ordner
.stversionsinnerhalb des freigegebenen Verzeichnisses. Ein eigener Pfad ist konfigurierbar, aber die Doku warnt ausdrücklich: „please ensure that it's on the same partition or filesystem as the regular folder path." -
Namenskonvention: zeitgestempelte Dateinamen.
-
Vier Modi: Trash Can (letzte Fassung,
cleanoutDays), Simple (N Fassungen pro Datei mit Zeitstempel), Staggered (altersbasiertes Ausdünnen), External (delegiert an ein Kommando). -
Der Staggered-Versioner dünnt logarithmisch aus:
Alter behalten wird 1 Stunde die älteste Version pro 30-Sekunden-Intervall 1 Tag die älteste Version pro Stunde 30 Tage die älteste Version pro Tag bis „Maximum Age" die älteste Version pro Woche „Maximum Age" = 0 bedeutet: für immer behalten.
Das ist die Antwort auf die Aufräumfrage, falls DioNotes je Historie bekommt: konstanter Overhead statt unbegrenztem Wachstum, ohne Diff-Rekonstruktion, ohne Datenbank. Vier Zeilen Regel.
3. Was ein Append-Log konkret kosten würde
Der Vorschlag aus der Beratungsrunde: ein Append-Log neben jeder Notiz, aus dem die .md als lesbare Sicht
materialisiert wird. Undo über Sitzungen, Historie und Konfliktauflösung fielen als Nebenprodukt an.
Der Vorschlag ist technisch seriös — das ist Event Sourcing, und es liefert tatsächlich alles drei. Die Frage ist nicht, ob er funktioniert, sondern was er kostet.
3.1 Größenwachstum — belegte Zahlen
Die maßgebliche Zahl stammt aus dem offiziellen Benchmark-Repo von Kevin Jahns, dem Yjs-Autor, das die Yjs-README selbst verlinkt (https://github.com/dmonad/crdt-benchmarks). Gemessen wird der bekannte B4-Editier-Trace: 182.315 Einfügungen, 77.463 Löschungen, 104.852 Zeichen im Endtext.
| Yjs 13.6.11 | Automerge 2.1.10 | |
|---|---|---|
Dokumentgröße gemerged (docSize) |
159.929 B ≈ 1,53× Rohtext | 129.116 B ≈ 1,23× Rohtext |
Summe aller Einzel-Updates (updateSize) |
7.306.126 B ≈ 70× Rohtext | — |
| Durchschnittliche Update-Größe | 29 B | — |
| Parse-Zeit | 39 ms | 1.805 ms |
Das ist die eigentliche Kostenaussage, und sie ist zweigeteilt (belegt):
- Ein zusammengefasstes CRDT-Dokument ist erstaunlich billig — 1,2 bis 1,5× des Rohtextes, mit voller Historie. Das ist deutlich günstiger, als man intuitiv erwartet.
- Ein ungekürzter Append-Log — also genau das, was der Vorschlag beschreibt — kostet rund 70× den Rohtext. Der Unterschied zwischen 1,5× und 70× ist die Compaction. Ein Log, der nur angehängt und nie zusammengefasst wird, liegt beim oberen Wert.
Bei der Skalierung B4×100 (10,5 Mio. Zeichen) bleibt die Dokumentgröße bei 1,53×, der Arbeitsspeicher steigt aber auf 327,1 MB — etwa 31× des Textes (belegt, dieselbe Quelle).
Die offizielle Yjs-FAQ ordnet das ein (https://docs.yjs.dev/api/faq): „Size is very rarely a practical problem as long as you deal with human-entered text input." Das ist ein fairer Einwand gegen eine reine Größen-Argumentation — und der Grund, warum der Ausschlussgrund in 3.4 steht und nicht hier.
Was daraus für DioNotes folgt (Ableitung):
- Eine Sidecar-Datei pro Notiz. Ein Workspace mit 500 Notizen enthält 1.000 Dateien statt 500. Der Notiz-Baum müsste die Hälfte davon ausblenden; jedes Backup-, Sync- und Suchwerkzeug des Nutzers sieht sie.
- Autosave koppelt an den Speicherplatz. Die Auslöser-Entscheidung aus #18 würde zur Speicherplatz-Entscheidung: „Autosave nach Tipppause" erzeugt Größenordnungen mehr Log-Einträge als „manuell mit Strg+S". Diese Kopplung existiert bei keiner anderen Option aus Abschnitt 4.
3.2 Aufräumen: bei CRDT-Logs nicht vorgesehen
Der wichtigste Befund dieses Abschnitts, und er ist aus Maintainer-Mund belegt.
Automerge — Peter van Hardenberg (Maintainer) in Issue #799, 24.12.2023, wörtlich:
„'Compact' could mean a few different things here — one is whether you can reduce the on-disk size of an Automerge document. This happens automatically when you call
save(). It quite literally compacts the history into a very efficient columnar representation.Discarding the history is not currently possible without replacing the document: if you don't have the same history, you can't merge the same changes. That's because we use a git-like commit hash for each change to ensure integrity."
„Shallow clone" und „cherry pick" nennt er als nützlich, verwandt und „not yet implemented"; nachgefasst am 23.09.2024: „I don't know when it will happen." Der Automerge-2.0-Blogpost kündigt dasselbe schon seit Januar 2023 an (https://automerge.org/blog/automerge-2/, Abschnitt „History Management"):
„Today the best way to remove something from an Automerge document's history is to recreate the document from scratch or to reset to a time before that change went in."
Orion Henry (Automerge-Core) in Issue #348, 25.04.2022:
„the solutions really are to either have the nodes coordinate a total document reset every so often —
throwing out the old document and starting fresh". Issue #348 wurde am 21.08.2026 geschlossen mit dem
Hinweis auf hexane, den neuen Storage-Engine — der laut offiziellem Blogpost
(https://automerge.org/blog/2026-july/) „around 2–9× faster" ist, also Kompression und Performance
liefert, keine History-Truncation. Die Schließung bedeutet nicht, dass das Feature existiert.
Yjs — die README nennt den Grund formal (https://github.com/yjs/yjs/blob/main/README.md, Abschnitt „Yjs CRDT Algorithm"):
„CRDTs that are suitable for shared text editing suffer from the fact that they only grow in size. […] We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs."
Im Quellcode bestätigt: Item.gc() ersetzt beim normalen Löschen nur den content durch
ContentDeleted(len) — das komplette Item-Struct mit id{client,clock}, origin, rightOrigin, parent
und der Reihenfolge-Verlinkung bleibt stehen
(https://github.com/yjs/yjs/blob/main/src/structs/Item.js). Und das Zusammenfassen von Updates hilft nicht:
„Note that this feature only merges document updates and doesn't garbage-collect deleted content"
(https://docs.yjs.dev/api/document-updates). Wer Snapshots will, muss GC ganz abschalten — der Code wirft
sonst: if (originDoc.gc) { throw new Error('Garbage-collection must be disabled in \originDoc`!') }`
(https://github.com/yjs/yjs/blob/main/src/utils/Snapshot.js).
Das Gegenargument, das fairerweise dazugehört. Das offizielle Automerge-Cookbook (https://automerge.org/docs/cookbook/modeling-data/) schreibt:
„Martin Kleppmann did some experiments with a benchmark document to see how much space would be saved by discarding history […] The savings are not all that great, which is why we haven't prioritised history truncation so far."
Das ist ernst zu nehmen — es gilt für Kleppmanns Messfall: ein großes, weitgehend linear getipptes Dokument mit wenigen Actor-IDs. Eine Notiz-App hat das umgekehrte Profil: viele kleine Dokumente, die über Jahre in vielen kurzen Sitzungen angefasst werden, also viele Actor-IDs pro Dokument. Ob die Ökonomie dort kippt, ist für dieses Dokument nicht belegt — die 70×-Zahl aus 3.1 legt es nahe, ist aber am B4-Trace gemessen, nicht am Notiz-Profil.
Fazit dieses Abschnitts: Aufräumen heißt bei einem CRDT-Log entweder gar nichts (Yjs behält Tombstones konstruktionsbedingt) oder „Dokument wegwerfen und neu anfangen" (Automerge). Genau denselben Kompromiss fährt Joplin mit dem Rebase der ältesten überlebenden Revision (2.2) — nur innerhalb einer Datenbank, wo er billig ist. Neben jeder Notiz im Dateisystem ist er es nicht.
3.3 Sidecar-Dateien in Sync-Ordnern — belegte Risiken
Ob die bloße Verdopplung der Dateizahl schadet, ist nicht belegt; die harten Limits sprechen sogar dagegen (Dropbox: „Users shouldn't sync more than 300.000 files" und „1.000.000 write operations per namespace in a 24 hour period", https://help.dropbox.com/plans/large-deployments; OneDrive: „no more than a total of 300.000 items", https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa). Bei typischen Workspace-Größen ist das unkritisch.
Belegt sind dagegen zwei andere Fallstricke, und beide treffen ein Sidecar-Design härter als eine einzelne Datei:
- Platzhalter-Dateien werden als gelöscht interpretiert. Obsidians offizielle Sync-FAQ (https://help.obsidian.md/sync/faq): „Cloud storage services like OneDrive and Dropbox offer 'Files on-demand' or 'Online-only files' features […] However, since these files are not always available locally, Obsidian Sync will interpret them as deleted, leading to their removal from your remote vault." Ergänzend (https://help.obsidian.md/sync-notes): „iCloud Drive on Windows may lead to file duplication or corruption." Und die architektonische Begründung: „Obsidian requires access to the entire vault for its features […] This makes it difficult for some services to function reliably with Obsidian."
- Das bloße Nachsehen, ob eine Datei existiert, kann die App einfrieren. Apples Technote TN3150
(https://developer.apple.com/documentation/technotes/tn3150-getting-ready-for-data-less-files):
„Materializing a dataless file can take time […] the app may become unresponsive. If an app's main
queue remains unresponsive for a long time, the system may terminate the app and trigger a watchdog
crash." Als auslösende Aktionen nennt Apple ausdrücklich „enumerating a directory's contents and
checking whether a file exists", und: „
statandgetattrlistboth trigger the materialization of any intermediate folders." Für jede Notiz die zugehörige Log-Datei zu suchen oder zu statten ist genau dieses Muster. (Die Technote gilt für Apple-Plattformen; DioNotes ist Windows-only, aber der Mechanismus — Platzhalter-Dateien, die beim Zugriff nachgeladen werden — ist bei OneDrive Files On-Demand derselbe. Übertragung: Vermutung.)
Apples eigene Empfehlung für mehrteilige Dokumente ist bezeichnend: File Packages statt nebeneinander-
liegender Dateien („If your document data format consists of multiple distinct pieces, use a file
package", https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForDocumentsIniCloud.html).
Für DioNotes untauglich — dann wäre die .md keine eigenständige Datei mehr, und genau das ist das
Versprechen.
Nicht als Beleg verwendbar (gegengeprüft und verworfen): Zotereos Warnung „extremely likely to corrupt your database" betrifft SQLite und File Locking, nicht Sidecar-Strukturen. git-annex' Warnung gegen Dropbox-Ordner beruht auf Case-Insensitivity und Symlink-Handling. Für DVC gibt es keine offizielle Warnung, nur Community-Threads.
3.4 Das Zwei-Wahrheiten-Problem — der eigentliche Einwand
Dieser Punkt braucht keine externe Quelle, er folgt aus den Rahmenbedingungen des Projekts.
DioNotes hat keinen Filewatcher (#16, Rahmenentscheidung) und rechnet ausdrücklich damit, dass Dropbox, Syncthing oder Git die Dateien ändern, während die App läuft (#20).
Damit gilt für einen Append-Log:
- Ändert jemand die
.mdvon außen, weiß der Log nichts davon. Der Log sagt „Inhalt X", die Datei sagt „Inhalt Y". Zwei Wahrheiten. - Beim nächsten Materialisieren gewinnt der Log — und überschreibt die externe Änderung stillschweigend. Das ist kein Randfall, sondern der Normalfall bei jedem Sync-Konflikt.
- Der Ausweg wäre, die externe Änderung als neuen Eintrag in den Log einzuspeisen (Diff gegen den zuletzt materialisierten Stand). Das ist machbar — aber es setzt genau die Konflikterkennung voraus, die #20 erst noch entscheiden muss, und macht sie von einer Kann- zu einer Muss-Anforderung.
Die entscheidende Beobachtung: Beim „Datei ist die Wahrheit"-Modell gibt es dieses Problem nicht. Eine Sicherungskopie, die veraltet ist, ist eine weniger nützliche Sicherungskopie — kein inkonsistenter Zustand. Die Datei ist immer richtig, weil sie per Definition die Wahrheit ist. Der Append-Log führt eine Korrektheitslücke ein, die das schlichte Verfahren gar nicht erst hat.
3.5 Bewertung am Versprechen
Der Bewertungsmaßstab aus dem Projektkontext: „Der Nutzer kann das Werkzeug jederzeit verlassen, seine Dateien sind mit jedem Editor lesbar."
Ein Append-Log, aus dem die .md materialisiert wird, degradiert die .md zur Ausgabe. Formal bleibt sie
lesbar — praktisch ist sie es nicht mehr im Sinne des Versprechens, denn:
- Eine Änderung an ihr ist wirkungslos oder wird verworfen, sobald DioNotes das nächste Mal materialisiert. Der Nutzer darf seine Dateien dann nicht mehr mit einem anderen Editor bearbeiten, sondern nur noch lesen.
- Das Werkzeug zu verlassen, hieße die Historie zurückzulassen — sie steckt im Log, nicht in der Datei.
Der Unterschied zu einer Sicherungskopie ist damit kein gradueller: Eine Sicherungskopie ist zusätzliche Information neben der Wahrheit. Ein Log ist die Wahrheit, und die Datei wird zur Kopie. Das ist genau die Umkehrung, die das Versprechen bricht.
4. Die billigeren Mittelwege
Zur Einordnung: Alle drei sind Aufsätze auf ein unverändertes Schreibverfahren. Keiner verlangt, dass #19 heute irgendetwas anders entscheidet. Sie sind hier aufgeführt, damit die Größenordnung des „später" bekannt ist — nicht als Empfehlung, heute etwas davon zu bauen.
4.1 Eine einzelne Sicherungskopie der Vorfassung
Der billigste Weg: Vor dem Umbenennen die Zieldatei nach <name>.md.bak (oder in .dionotes/bak/) kopieren.
Eine Fassung, überschrieben bei jedem Speichern.
- Kosten: eine Datei pro je gespeicherter Notiz, Größe = Größe der Notiz. Bei 500 Notizen à 4 KB also ~2 MB. Kein Wachstum über die Zeit, weil jede Sicherung die vorige ersetzt. Aufräumen entfällt.
- Nutzen: deckt genau einen Fall ab — „ich habe gerade etwas kaputtgespeichert". Kein Undo über Sitzungen, keine Historie.
- Präzedenzfall: Logseq macht im Kern nichts anderes, nur mit sechs statt einer Fassung und nur bei Konflikt (2.3).
- Fallstrick: Wenn die Sicherung neben der Notiz liegt, sieht der Nutzer sie im Ordner und im Sync —
und der Notiz-Baum müsste sie ausblenden, ohne die Punkt-Regel aufzuweichen. In
.dionotes/liegt sie außer Sichtweite, wird aber beim Löschen einer Notiz zur Waise. Beides lösbar, beides eine Entscheidung für später.
4.2 Zeitstempel-Ordner mit logarithmischer Ausdünnung (das Syncthing-Modell)
Ein Ordner .dionotes/versions/<relativer-pfad>/<zeitstempel>.md, ausgedünnt nach dem Muster aus 2.5.
- Kosten: Volltext-Kopien, aber durch die Ausdünnung mit konstantem Overhead statt unbegrenztem Wachstum. Bei den Syncthing-Buckets landet man pro Notiz nach einem Jahr bei grob 50–100 Fassungen (Abschätzung, nicht belegt) — für eine 4-KB-Notiz also ein paar hundert Kilobyte.
- Nutzen: echte Historie mit Zeitpunkten, Undo über Sitzungen, kein Diff-Algorithmus nötig.
- Belegtes Vorbild in Produktion: Syncthings
.stversionsmit vier Modi und den Ausdünn-Buckets (2.5, https://docs.syncthing.net/users/versioning.html). - Warum das die beste Nachrüst-Option wäre: Es braucht keine Datenbank, kein Diff-Format, keine Kettenrekonstruktion wie bei Joplin (2.2) — und es vermeidet Logseqs Aufräumfehler, weil die Ausdünnung altersbasiert statt anzahlbasiert-pro-Datei ist.
4.3 Git-Repository im Workspace
Die Variante, die manche Nutzer heute schon von Hand fahren — und die Logseqs „View page history" bereits voraussetzt (2.3).
Belegte Kosten:
- Pro Commit entstehen mindestens drei lose Objekte (Blob + Root-Tree + Commit), pro Verzeichnisebene ein
weiteres Tree-Objekt. Format: zlib-komprimierter Header + Inhalt unter
.git/objects/<2>/<38>(https://git-scm.com/book/en/v2/Git-Internals-Git-Objects). - Lose Objekte enthalten den vollen Blob, keine Deltas — Delta-Kompression gibt es erst beim Packen. Eine Ein-Zeichen-Änderung an einer 5-KB-Notiz erzeugt einen kompletten neuen komprimierten Blob (Ableitung aus derselben Quelle).
gc.auto= 6700 lose Objekte,gc.autoPackLimit= 50 Packs,gc.pruneExpire= 2.weeks.ago,gc.autoDetach= true (https://git-scm.com/docs/git-config, https://git-scm.com/docs/git-gc).- Sobald Housekeeping ausgelöst wird, laufen laut Doku alle Housekeeping-Aufgaben mit (rerere, worktrees, reflog).
Ableitung für DioNotes: 6700 ÷ ~3 Objekte je Commit ≈ ~2.200 Commits bis zum ersten automatischen
Repack. Bei „ein Commit pro Speichern" mit Autosave ist das eine Frage von Wochen, nicht von Jahren. Der
Repack läuft dann im Hintergrund, schreibt große .pack-Dateien und löscht tausende lose Dateien auf einen
Schlag — mitten in einem Ordner, den womöglich Dropbox oder Syncthing beobachtet. Für den Sync-Client sieht
das aus wie ein Änderungssturm (Ableitung, mittlere Konfidenz). .git/ gehörte damit zwingend aus dem
Sync ausgenommen.
Die Rust-Seite (belegt, crates.io/docs.rs):
| Crate | Version | Downloads gesamt | C-Toolchain nötig? |
|---|---|---|---|
git2 (libgit2-Bindings) |
0.21.0 | ~110 Mio. | Ja — libgit2-sys kompiliert ~180k Zeilen C via cc (kein CMake) |
gix / gitoxide |
0.87.1 | ~44 Mio. | Nein — Feature-Doku wörtlich: „No C toolchain is involved." |
gix beschreibt sich als „pure Rust implementation of Git", testet Windows in CI („failures do prevent
releases") und deckt commit, log, status, diff, reset ab. Der Haken ist der Reifegrad: Der Haupt-Einstiegs-
punkt gix steht laut eigenem Stability-Tier-Schema noch auf „Initial Development", entsprechend
API-Churn bei jedem Minor. Unter Windows nutzt libgit2 WinHTTP, OpenSSL ist für lokale Repo-Operationen nicht
nötig (Ableitung aus der Windows-Verlinkung in build.rs).
Bewertung: Git ist der einzige Weg, der Historie und Diff und Branching liefert — und der einzige, der eine schwergewichtige Dependency plus unbegrenztes Wachstum plus periodisch teures Repacking mitbringt. Für „Undo über Sitzungen" ist es mit Abstand die teuerste Antwort. Bemerkenswert ist, was die untersuchten Editoren daraus machen: Zettlr empfiehlt Git nicht (2.4), Logseq benutzt es nur lesend und setzt voraus, dass der Nutzer es selbst eingerichtet hat (2.3). Niemand richtet dem Nutzer ein Repo ein.
4.4 Kostenvergleich
| Option | Zusätzliche Dateien | Wachstum | Aufräumen | Dependency | Bricht das Versprechen? |
|---|---|---|---|---|---|
| Nichts tun (heute) | 0 | — | — | keine | nein |
| Eine Sicherungskopie (4.1) | 1 pro gespeicherter Notiz | keins | entfällt | keine | nein |
| Zeitstempel-Ordner (4.2) | N pro Notiz, ausgedünnt | beschränkt | Ausdünn-Regel, ~1 Funktion | keine | nein |
| Git im Workspace (4.3) | .git/, ≥3 Objekte pro Commit |
unbeschränkt | git gc, teuer |
gix oder git2 |
nein |
| Append-Log (Abschnitt 3) | 1 Log pro Notiz | ≈ 70× Rohtext ungekürzt, ≈ 1,2–1,5× gemerged | von den Bibliotheken nicht vorgesehen (Automerge: Dokument neu anlegen; Yjs: Tombstones bleiben) | CRDT-/Log-Bibliothek | ja — die .md wird zur Kopie |
5. Die eigentliche Frage: Schließt temp+rename eine spätere Historie aus?
Nein. Die Begründung in vier Schritten.
5.1 Das Schreibverfahren ist für Historie günstig, nicht hinderlich
Wie in Abschnitt 1 hergeleitet: Historie braucht Auslöser und Vorfassung. Beim temp+rename-Verfahren ist die Vorfassung im Moment vor dem Umbenennen noch vollständig auf der Platte. Der Einbau einer Historie wäre eine Einfügung von wenigen Zeilen an einer Stelle, an der man ohnehin schon steht:
schreibe_notiz(pfad, inhalt):
prüfe Konflikt (aus #20)
── hier ließe sich später die Vorfassung wegsichern ──
schreibe inhalt nach pfad.tmp
sync
benenne pfad.tmp nach pfad um
Das Gegenteil gilt für den naiven Weg: fs::write(pfad, inhalt) zerstört die Vorfassung, bevor irgendjemand
sie sichern könnte. Wer dort Historie nachrüstet, muss die Datei zusätzlich vorher lesen. Das ist immer noch
billig — aber es ist der teurere der beiden Wege, und er ist zugleich der, den #19 aus Gründen der
Absturzsicherheit ohnehin ausschließen will.
Damit ist der Einwand in seiner konkreten Form widerlegt: Die Entscheidung, die #19 zu treffen hat, macht Historie nicht teurer. Sie macht sie billiger.
5.2 Was Historie tatsächlich teuer machen würde
Damit das Ergebnis belastbar ist, hier die Gegenprobe — die Entscheidungen, die den Einwand berechtigt machen
würden. Keine davon ist die Wahl zwischen rename und write:
| Falle | Warum sie teuer wird | Kosten sie zu vermeiden |
|---|---|---|
| Mehrere Schreibstellen. Autosave schreibt hier, „Notiz verlassen" dort, „Fenster schließen" an dritter Stelle. | Historie muss an jeder Stelle nachgezogen werden, und jede vergessene Stelle ist ein Loch in der Historie, das man erst durch einen Bug bemerkt. | Eine Funktion, die schreibt. Alle Auslöser aus #18 rufen dieselbe. Kostet heute nichts. |
| Kein Notiz-Begriff, nur Pfade. | Historie, die am Pfad hängt, reißt beim Umbenennen ab (#23). Obsidian hat genau dieses Problem, dokumentiert („snapshots are stored with the absolute path to the note"). | Entweder akzeptieren (Obsidian tut es) oder später eine stabile ID einführen. Beides nachrüstbar; die Kette bräche dann nur für Umbenennungen vor der Einführung. |
| Umbenennen als Löschen+Neuanlegen implementiert. | Zerreißt jede Historie und erzeugt Waisen im Ablageort. | Umbenennen als Umbenennen implementieren. Ist in #23 ohnehin die naheliegende Wahl. |
| Der Log-Ansatz aus dem Vorschlag. | Siehe Abschnitt 3 — er ist die einzige der hier betrachteten Optionen, die schwer umkehrbar ist. | Ihn nicht bauen. |
Nur die erste Zeile ist eine echte Auflage an #19 — und sie kostet heute exakt nichts, weil es aktuell
noch überhaupt keine Schreibstelle im Code gibt (verifiziert: keine Fundstelle für fs::/write/File::
unter src/; der Editor nimmt Eingaben entgegen und verwirft sie, so beschrieben in
#16).
5.3 Die Asymmetrie läuft andersherum
Der Vorschlag aus der Beratungsrunde begründet sich damit, dass die Schreib-Entscheidung schwer umkehrbar sei. Prüft man beide Richtungen, ergibt sich das Gegenteil:
| Richtung | Aufwand |
|---|---|
| „Datei ist die Wahrheit" → „Datei plus Sicherungen" | Ergänzung. Eine Funktion erweitern, ein Ordner, eine Aufräumregel. Bestehende Notizen funktionieren unverändert weiter, die Historie beginnt eben ab Einführung. Kein Datenmigrations-Schritt, kein Bruch für den Nutzer. |
| „Log ist die Wahrheit" → „Datei ist die Wahrheit" | Migration. Jede Notiz aus dem Log materialisieren, die Logs entfernen oder verwaisen lassen, die gesamte gesammelte Historie wegwerfen oder in ein neues Format überführen — und den Nutzern erklären, warum ihre Historie weg ist. |
Wer sich die Option offenhalten will, hält sie also gerade dadurch offen, dass er den Log nicht baut. Der Log ist die Einbahnstraße, nicht das schlichte Schreiben.
Obsidian ist der empirische Beleg: Das Werkzeug wurde 2020 veröffentlicht, hat von Anfang an lose .md-Dateien
geschrieben, und File recovery kam später als Core-Plugin obendrauf — vollständig außerhalb des Vault, ohne
das Dateiformat oder das Schreibverfahren anzufassen (belegt durch den Aufbau des Plugins, s. 2.1;
das genaue Erscheinungsdatum des Plugins ist nicht belegt).
5.4 Belastbarkeit dieser Antwort
Die Antwort steht auf zwei Beinen unterschiedlicher Härte:
- Hart (belegt): Obsidian betreibt eine produktive Historie für lose
.md-Dateien als aufgesetzte Schicht, die vom Schreibverfahren des Kerns entkoppelt ist und außerhalb des Datenordners lebt. Ein Werkzeug mit derselben Ausgangslage wie DioNotes hat das Problem also nachweislich nachträglich gelöst. - Weich (Argumentation, nicht durch Quelle belegt): Dass genau eine Schreibstelle die einzige load-bearing Auflage ist, ist eine Schlussfolgerung aus der Zerlegung in Abschnitt 1, kein Zitat. Sie ist aber schwer zu bestreiten: Eine Schicht über N Aufrufstellen einzuziehen kostet N Änderungen, über einer Aufrufstelle eine.
Fazit für #19: Der Einwand ist entkräftet. Die Schreib-Entscheidung darf allein nach den Kriterien aus #19 fallen — Absturzsicherheit, Sichtbarkeit der temporären Datei, Dateirechte, Aufräumen. Historie legt ihr genau eine Auflage auf, und die ist heute gratis: eine einzige Funktion, durch die alles Schreiben läuft.
6. Randbefunde für die Nachbartickets
Bei der Recherche fielen Belege an, die #22 nicht beantworten, aber #19 und #20 direkt betreffen. Sie stehen hier, damit sie nicht verlorengehen — nicht als Entscheidung.
Für #19 (Schreibverfahren):
- Weder Rust noch Microsoft dokumentieren
renameals atomar. Das Wort „atomic" kommt weder in https://doc.rust-lang.org/std/fs/fn.rename.html noch in derMoveFileExW-Doku vor. Rust garantiert nur „replacing the original file iftoalready exists" und nenntMoveFileExWmit Fallback aufSetFileInformationByHandle. Man verlässt sich auf undokumentiertes NTFS-Verhalten (belegt durch Abwesenheit). - Microsoft empfiehlt für genau diesen Anwendungsfall
ReplaceFile, nichtMoveFileEx. Aus https://learn.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf, Abschnitt „Applications updating a single file with 'document-like' data": „A common approach is to write the document to a new file, then replace the original file with the new one. One method to do this is with theReplaceFileAPI." - Temp+rename verliert Metadaten. Die
ReplaceFileW-Remarks führen als Vorteil auf, dass die Funktion creation time, short file name, object identifier, DACLs, security resource attributes, encryption, compression und named streams der Originaldatei erhält — was per Kontrast belegt, dass ein nacktes temp+rename genau diese verliert. Die File ID ändert sich sogar beiReplaceFile. Kein untersuchtes Rust-Crate (tempfile,atomicwrites,atomic-write-file) nutztReplaceFile;atomic-write-filedokumentiert die Verluste immerhin explizit („No support for preserving timestamps, ACLs, extended attributes…"). → Praktische Folge für #19: Ein „erstellt am" gehört ins Frontmatter der.md, nicht ins Dateisystem. - Das realistischste Fehlerbild ist
ERROR_SHARING_VIOLATION, wenn Defender oder ein Sync-Client die Zieldatei ohneFILE_SHARE_DELETEoffen hält (https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew).tempfile::NamedTempFile::persistgibt die temporäre Datei imPersistErrorzurück — genau für den Retry-Fall gebaut. Ebenfalls belegt:persistsynchronisiert weder Inhalt noch Verzeichnis. - Die temporäre Datei muss im selben Verzeichnis liegen —
fs::renamescheitert über Volume-Grenzen („This will not work if the new name is on a different mount point"), und Syncthing warnt bei seinem eigenen Versions-Ordner mit derselben Begründung. fs-errlöst das Problem nicht (es ist nur ein Drop-in-Ersatz fürstd::fsmit besseren Fehlermeldungen), ist aber die naheliegende Ergänzung, um überhaupt zu sehen, welche Datei die Sharing-Violation geworfen hat.
Für #20 (Zeitstempel-Konflikt):
- Zettlrs mtime-Regel ist bewusst
modtime <= ourModtime → ignorieren, nicht Gleichheitsprüfung, mit wörtlicher Begründung im Code: „to prevent sluggish cloud synchronization services (e.g. OneDrive and Box) from having text appear to 'jump'". Danach folgt ein Inhaltsvergleich, weil Cloud-Dienste Dateien anfassen, ohne den Inhalt zu ändern. Ein reiner mtime-Vergleich fängt das nicht ab. - Logseq vergleicht ebenfalls den Inhalt vor dem Schreiben und schreibt bei Abweichung gar nicht. Beide untersuchten Open-Source-Editoren lösen den Konflikt über Inhalt, nicht über Zeitstempel allein.
- Beide brauchen einen Self-Echo-Filter, damit der eigene Schreibvorgang nicht als externe Änderung
zurückkommt (Zettlr:
_ignoreChanges-Liste; Logseq: Inhaltsvergleich). Das ist erst relevant, falls je ein Filewatcher dazukommt — aber es erklärt, warum ein Watcher nicht „nur ein Watcher" ist. - Dropbox nennt als dokumentierte Ursache Nr. 3 für Conflicted Copies wörtlich „applications with an auto-save feature" (https://help.dropbox.com/organize/conflicted-copy). Das ist ein Argument für #18, Autosave zu entprellen statt pro Tastendruck zu schreiben.
- Syncthings Konfliktnamen sind
<name>.sync-conflict-<date>-<time>-<modifiedBy>.<ext>, und diese Dateien werden „treated as normal files after they are created, so they are propagated between devices" — sie tauchen also im Notiz-Baum auf, wenn nichts sie ausfiltert.
7. Belegt vs. Vermutung — Übersicht
| Aussage | Status |
|---|---|
Obsidian: Snapshots außerhalb des Vault, %APPDATA%\Obsidian\, Volltext, 5 Min / 7 Tage Default |
Belegt (help.obsidian.md) |
| Obsidian: Snapshots syncen nicht, absolute Pfade, brechen nach Vault-Umzug | Belegt (help.obsidian.md) |
| Obsidian: Snapshots liegen konkret in IndexedDB/LevelDB | Vermutung — starkes Indiz über die Lockdown-Mode-Einschränkung, kein wörtlicher Beleg |
| Obsidian: Serialisierungsformat der Snapshots | Nicht belegbar — Closed Source, nirgends dokumentiert |
| Obsidian: Schreibverfahren des Kerns (atomar oder nicht) | Nicht belegbar — keine offizielle Aussage |
Obsidian: Vault.process ist „atomar" nur prozessintern, nicht auf FS-Ebene |
Lesart — die Doku macht die Unterscheidung nicht explizit |
Joplin: SQLite statt .md, revisions-Tabelle, diff-match-patch, 10 Min / 90 Tage |
Belegt (joplinapp.org + Quellcode) |
| Joplin: Rebase der ältesten überlebenden Revision beim Aufräumen | Belegt (Quellcode-Kommentar + Implementierung) |
| Joplin: konkrete Größe der Revisions-Tabelle in der Praxis | Nicht belegbar — keine offizielle Zahl |
Logseq: logseq/bak/<datei>/<ISO>.md, nur bei Konflikt/Schreibfehler/externer Änderung |
Belegt (Quellcode 0.10.9) |
| Logseq: „reserve the latest 6 version files", nur beim Schreiben in denselben Ordner | Belegt (Quellcode) |
| Logseq: bak-Ordner werden GB-groß | Nicht belegbar — belegt ist unbegrenztes Ordner-Wachstum, keine Primärquelle nennt eine Größe |
Logseq: nicht-atomares writeFileSync, chokidar-Watcher, Diff-Modal bei Konflikt |
Belegt (Quellcode) |
| Logseq: Undo ist reines In-Memory-Atom, überlebt Neustart nicht | Belegt (Quellcode + Doku) |
| Zettlr: keine eingebaute Historie, Doku verweist auf Cloud-Backup | Belegt (docs.zettlr.com) |
Zettlr: fs.writeFile, nicht atomar; mtime-Filter „nur neuer, nicht ungleich"; Konflikt-Dialog |
Belegt (Quellcode master @ 86c6c56) |
| Zettlr: kein offizielles Git-Plugin, kein Plugin-System | Belegt (404 + Maintainer-Aussage in Discussion #3108) |
Syncthing: .stversions, Zeitstempel-Namen, vier Modi, staggered-Buckets |
Belegt (docs.syncthing.net) |
Git: gc.auto = 6700, gc.autoPackLimit = 50, ≥3 lose Objekte pro Commit |
Belegt (git-scm.com) |
| Git: „~2.200 Commits bis zum ersten Repack" | Ableitung aus den belegten Defaults |
gix braucht keinen C-Compiler, git2 schon |
Belegt (docs.rs Feature-Doku, libgit2-sys/build.rs) |
| Ausgedünnte Zeitstempel-Historie ≈ 50–100 Fassungen pro Notiz nach einem Jahr | Abschätzung, nicht belegt |
| „Historie ist eine Schicht über dem Schreiben" | Belegt am Beispiel Obsidian, im Übrigen Argumentation |
| „Eine Schreibstelle ist die einzige echte Auflage" | Argumentation, nicht durch Quelle belegt — aber aus der Zerlegung in Abschnitt 1 zwingend |
| Zwei-Wahrheiten-Problem des Append-Logs | Argumentation aus den Projekt-Rahmenbedingungen (kein Filewatcher, externer Sync erwartet) |
| CRDT-Dokument gemerged ≈ 1,2–1,5× Rohtext; Summe der Einzel-Updates ≈ 70× Rohtext (B4-Trace) | Belegt (dmonad/crdt-benchmarks, Repo des Yjs-Autors) |
| Automerge: „Discarding the history is not currently possible without replacing the document" | Belegt (Maintainer-Zitat, Issue #799) |
| Automerge: shallow clone / history truncation seit 2023 angekündigt, bis heute nicht implementiert | Belegt (Blogpost + Issue-Verlauf + API-Oberfläche) |
| Yjs: Tombstones sind konstruktionsbedingt nicht sammelbar, Update-Merge räumt nicht auf | Belegt (README, Item.js, docs.yjs.dev) |
| Automerge-Cookbook: „The savings are not all that great" (Gegenargument) | Belegt — gilt für Kleppmanns Messfall (großes Dokument, wenige Actor-IDs) |
| Ob die Ökonomie beim Notiz-Profil (viele kleine Dokumente, viele Sitzungen) kippt | Nicht belegt — die 70×-Zahl legt es nahe, ist aber am B4-Trace gemessen |
| Obsidian Sync: „Files on-demand"-Platzhalter werden als gelöscht interpretiert | Belegt (help.obsidian.md) |
| Apple TN3150: Existenzprüfung/Verzeichnis-Enumeration kann Materialisierung und Watchdog-Crash auslösen | Belegt — Übertragung auf OneDrive Files On-Demand unter Windows ist Vermutung |
| Dass die bloße Verdopplung der Dateizahl in Sync-Ordnern schadet | Nicht belegt — die 300.000er-Limits sprechen eher dagegen |
Quellenliste
Obsidian
- File recovery (Core-Plugin): https://obsidian.md/help/plugins/file-recovery
- How Obsidian stores data: https://obsidian.md/help/data-storage
- Vault API Guide: https://docs.obsidian.md/Plugins/Vault
- Vault TypeScript-Referenz: https://docs.obsidian.md/Reference/TypeScript+API/Vault
DataAdapter.write: https://docs.obsidian.md/Reference/TypeScript+API/DataAdapter/write- Sync Version history: https://obsidian.md/help/sync/version-history
- Changelog 1.13.4 (Save-Race-Condition): https://obsidian.md/changelog/2026-07-30-desktop-v1.13.4/
- Changelog 1.6.0 (externe Änderungen): https://obsidian.md/changelog/2024-05-09-desktop-v1.6.0/
- Feature-Request „Snapshots im Vault" (Sekundärquelle): https://forum.obsidian.md/t/file-recovery-store-file-snapshots-in-the-vault-folder-instead-of-the-obsidians-system-folder/116677
Joplin
- Note History: https://joplinapp.org/help/apps/note_history/
- User profile / Datenablage: https://joplinapp.org/help/dev/spec/user_profile/
- Import & Export: https://joplinapp.org/help/apps/import_export/
Revision.ts: https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Revision.tsRevisionService.ts: https://github.com/laurent22/joplin/blob/dev/packages/lib/services/RevisionService.tsbuiltInMetadata.ts(Defaults): https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts
Logseq (alle Codebelege gegen Tag 0.10.9)
backup_file.cljs(bak-Ordner, Namensschema, Truncate): https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/backup_file.cljshandler.cljs(Schreiben,.recycle,backupDbFile): https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/handler.cljsfs/node.cljs(Vergleich vor dem Schreiben): https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/fs/node.cljswatcher_handler.cljs(Backup bei externer Änderung): https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/fs/watcher_handler.cljsfs_watcher.cljs(chokidar): https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/fs_watcher.cljscomponents/diff.cljs(Konflikt-Modal): https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/components/diff.cljsundo_redo.cljs(In-Memory-Undo): https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/modules/editor/undo_redo.cljspage_menu.cljs(„View page history" viagit log): https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/components/page_menu.cljs- Doku „Undo and Redo": https://github.com/logseq/docs/blob/master/pages/Undo%20and%20Redo.md
- Issue #3370 (Maintainer: „It's a safe net"): https://github.com/logseq/logseq/issues/3370
- Issue #4819 (Gründer: „cleanup backups using some cron scripts"): https://github.com/logseq/logseq/issues/4819
- PR #4421 (Einführung des Truncate): https://github.com/logseq/logseq/pull/4421
- Forum: Wunsch nach Off-Schalter (Sekundärquelle): https://discuss.logseq.com/t/add-option-to-disable-backups-logseq-bak-folder/33107
Zettlr (Codebelege gegen master @ 86c6c56)
- Workspaces / Backup-Empfehlung: https://docs.zettlr.com/en/first-time-users/workspaces.html
- Settings (Autosave, „Always load remote changes"): https://docs.zettlr.com/en/reference/settings.html
fsal/index.ts(writeTextFile): https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/index.tsdocuments/index.ts(saveFile,handleRemoteChange): https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/documents/index.tsfsal-watchdog.ts(chokidar): https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/fsal-watchdog.tsfsal-directory.ts(isGitRepository): https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/fsal-directory.ts- Discussion #3108 (kein Plugin-System): https://github.com/Zettlr/Zettlr/discussions/3108
- Issue #1050 (Git seit 2020 offen): https://github.com/Zettlr/Zettlr/issues/1050
Append-Log / CRDT (Automerge, Yjs)
- Automerge Issue #799 (Maintainer pvh: „Discarding the history is not currently possible…"): https://github.com/automerge/automerge/issues/799
- Automerge Issue #348 (Orion Henry: „a total document reset every so often"): https://github.com/automerge/automerge/issues/348
- Automerge 2.0, Abschnitt „History Management": https://automerge.org/blog/automerge-2/
- Automerge Cookbook, „Modeling Data → Performance" (Kleppmann: „The savings are not all that great"): https://automerge.org/docs/cookbook/modeling-data/
- Automerge
hexaneStorage-Engine (Kompression, keine Truncation): https://automerge.org/blog/2026-july/ - Yjs README, „Yjs CRDT Algorithm" (Tombstones): https://github.com/yjs/yjs/blob/main/README.md
- Yjs
src/structs/Item.js(Item.gc()): https://github.com/yjs/yjs/blob/main/src/structs/Item.js - Yjs
src/utils/Snapshot.js(GC muss aus sein): https://github.com/yjs/yjs/blob/main/src/utils/Snapshot.js - Yjs Document Updates („doesn't garbage-collect deleted content"): https://docs.yjs.dev/api/document-updates
- Yjs FAQ („Size is very rarely a practical problem"): https://docs.yjs.dev/api/faq
- crdt-benchmarks (Kevin Jahns, Zahlen zu B4): https://github.com/dmonad/crdt-benchmarks
Sidecar-Dateien in Sync-Ordnern
- Obsidian Sync FAQ („interpret them as deleted"): https://help.obsidian.md/sync/faq
- Obsidian, Notizen zwischen Geräten synchronisieren: https://help.obsidian.md/sync-notes
- Apple TN3150 „Getting ready for dataless files": https://developer.apple.com/documentation/technotes/tn3150-getting-ready-for-data-less-files
- Apple iCloud Design Guide (File Packages statt Sidecars): https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForDocumentsIniCloud.html
- Dropbox-Limits (300.000 Dateien, 1 Mio. Writes/24 h): https://help.dropbox.com/plans/large-deployments
- OneDrive-Limits (300.000 Items): https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa
Syncthing
- File Versioning: https://docs.syncthing.net/users/versioning.html
- Syncing / Conflict Handling: https://docs.syncthing.net/users/syncing.html
- Ignoring Files (
.stignore): https://docs.syncthing.net/users/ignoring.html
Git und Rust-Anbindung
- Git Internals — Git Objects: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
git-config(gc.auto,gc.autoPackLimit,gc.pruneExpire): https://git-scm.com/docs/git-configgit-gc: https://git-scm.com/docs/git-gcgit2: https://crates.io/crates/git2 · https://github.com/rust-lang/git2-rsgix/ gitoxide: https://crates.io/crates/gix · https://github.com/GitoxideLabs/gitoxide · https://docs.rs/gix/
Windows-Schreibverfahren (für #19, hier nur als Randbefund)
std::fs::rename: https://doc.rust-lang.org/std/fs/fn.rename.htmlMoveFileExW: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexwReplaceFileW(Liste der erhaltenen Attribute): https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew- Deprecation of TxF (Microsofts Empfehlung für „document-like data"): https://learn.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf
CreateFileW(ERROR_SHARING_VIOLATION,FILE_SHARE_DELETE): https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilewtempfile::NamedTempFile::persist: https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist- Dropbox „Conflicted copy" (Ursache: „applications with an auto-save feature"): https://help.dropbox.com/organize/conflicted-copy