Follow-up to #16, where a user found a mystery .txt-migration-done file sitting in their notes folder and asked whether their notes used to be .txt.
They didn't. Nothing is broken and no data is at risk. But we are leaving an internal bookkeeping crumb in the user's shared Documents folder, and it made someone think their vault had been silently migrated. That's worth cleaning up.
What the file actually is
crates/futo-notes-store/src/lib.rs:1289 — migrate_text_files(), called from bootstrap() at lib.rs:359:
- Look for
.txt-migration-done in the vault root. If it exists, return immediately.
- Otherwise
read_dir the vault root, rename every *.txt to *.md (with an (imported) suffix on collision).
- Write
.txt-migration-done containing 1.
Step 3 runs unconditionally — even when zero files were migrated (lib.rs:1347). So every install of the build carrying the rewritten store engine creates this file on first launch, including vaults that never contained a single .txt file. The reporter's Jul 28 timestamp is just when that build first opened their vault.
The .txt support itself dates to March (cdd869e7, 17e44f48): drop a .txt into the vault, get a note. The sentinel exists only so the sweep isn't repeated forever.
Why it's visible at all
It's a dotfile, and the reporter has "show hidden files" enabled in their file manager. It is one of seven internal files that already live next to notes:
.app-config.json .app-state.json .crashlogs .e2ee-state.json
.engagement-v1.json .note-preview-cache.json .txt-migration-done
The other six each hold live state. .txt-migration-done is the only one that is a permanent one-byte crumb with no ongoing function. Deleting it by hand is safe — the next launch re-runs the sweep (a no-op with no .txt files present) and rewrites it.
Secondary problem the sentinel creates
Once the sentinel exists, a .txt dropped into the vault is converted never. And note_id_from_relative_path only accepts .md (crates/futo-notes-core/src/files/paths.rs:140), so that file is invisible in the note list permanently — while the search indexer still indexes .txt and strips the suffix to build an id (crates/futo-notes-search/src/indexer.rs:322,351) that the store cannot open. The desktop watcher also still accepts .txt (apps/tauri/src-tauri/src/filesystem_watcher.rs:122,146) but nothing downstream converts it.
So the once-only sentinel quietly turned "drop a .txt, get a note" into a dead end, and left search able to return an unopenable id.
Also worth noting: the sweep is root-only (fs::read_dir(&self.root)), while the note walk is recursive to MAX_FOLDER_DEPTH (crates/futo-notes-store/src/vault.rs:113). A .txt in a subfolder was never migrated at all.
Possible fixes
A. Delete the sentinel, sweep on every bootstrap — recommended
Drop the sentinel read/write and keep the sweep. Add a one-line unlink so existing installs shed the file on next launch.
- Cost is negligible: one extra
read_dir of the root, when bootstrap() already walks the whole tree recursively.
- Fixes the dead end above — a
.txt added later becomes a note on the next launch, which is what the March feature promised.
- Smallest diff; removes the file class rather than relocating it.
- Behavior change that needs a decision: a
.txt a user has deliberately parked in their vault root gets renamed to .md on next launch instead of being ignored forever. I think that is the intended contract, but it is a change and needs a docs/spec/ line.
- While here, walk to the same depth as the note walk so subfolder
.txt files are covered.
B. Move the sentinel to app-private storage
Keeps once-only semantics and gets the file out of the user's folder.
futo-notes-store only knows root today, so this means threading a second app-support path through the store constructor in all three shells (Tauri, iOS NoteVault, Android NotesStorage).
- A vault moved between devices re-runs the sweep once. Harmless, but no longer strictly once.
- Leaves the
.txt dead end and the subfolder gap in place, and does nothing for the other six dotfiles.
C. Collapse all app-state files under a single hidden .futo/ directory
This is the fix for the complaint the reporter actually had — internal files sitting among their notes.
- Right long-term shape, but the wrong size for this report. It needs a real migration for six live files, one of which is
.e2ee-state.json. Losing or half-moving that is exactly the failure mode behind the conflict-copy-spam incident, and the sync path filters would need updating alongside it.
- Should be its own tracked cleanup, not folded into this fix.
Suggested scope
Do A now, with a regression test in crates/futo-notes-store/src/tests.rs, and file C separately.
bootstrap_migrates_txt_collisions_once_before_returning_the_snapshot (tests.rs:276) asserts the sentinel is a file at tests.rs:288 and locks the once-only behavior, so it has to be rewritten as part of A — the new test should assert the sentinel is absent after bootstrap and that a .txt added after a first bootstrap is migrated by the second.
Follow-up to #16, where a user found a mystery
.txt-migration-donefile sitting in their notes folder and asked whether their notes used to be.txt.They didn't. Nothing is broken and no data is at risk. But we are leaving an internal bookkeeping crumb in the user's shared Documents folder, and it made someone think their vault had been silently migrated. That's worth cleaning up.
What the file actually is
crates/futo-notes-store/src/lib.rs:1289—migrate_text_files(), called frombootstrap()atlib.rs:359:.txt-migration-donein the vault root. If it exists, return immediately.read_dirthe vault root, rename every*.txtto*.md(with an(imported)suffix on collision)..txt-migration-donecontaining1.Step 3 runs unconditionally — even when zero files were migrated (
lib.rs:1347). So every install of the build carrying the rewritten store engine creates this file on first launch, including vaults that never contained a single.txtfile. The reporter's Jul 28 timestamp is just when that build first opened their vault.The
.txtsupport itself dates to March (cdd869e7,17e44f48): drop a.txtinto the vault, get a note. The sentinel exists only so the sweep isn't repeated forever.Why it's visible at all
It's a dotfile, and the reporter has "show hidden files" enabled in their file manager. It is one of seven internal files that already live next to notes:
The other six each hold live state.
.txt-migration-doneis the only one that is a permanent one-byte crumb with no ongoing function. Deleting it by hand is safe — the next launch re-runs the sweep (a no-op with no.txtfiles present) and rewrites it.Secondary problem the sentinel creates
Once the sentinel exists, a
.txtdropped into the vault is converted never. Andnote_id_from_relative_pathonly accepts.md(crates/futo-notes-core/src/files/paths.rs:140), so that file is invisible in the note list permanently — while the search indexer still indexes.txtand strips the suffix to build an id (crates/futo-notes-search/src/indexer.rs:322,351) that the store cannot open. The desktop watcher also still accepts.txt(apps/tauri/src-tauri/src/filesystem_watcher.rs:122,146) but nothing downstream converts it.So the once-only sentinel quietly turned "drop a
.txt, get a note" into a dead end, and left search able to return an unopenable id.Also worth noting: the sweep is root-only (
fs::read_dir(&self.root)), while the note walk is recursive toMAX_FOLDER_DEPTH(crates/futo-notes-store/src/vault.rs:113). A.txtin a subfolder was never migrated at all.Possible fixes
A. Delete the sentinel, sweep on every bootstrap — recommended
Drop the sentinel read/write and keep the sweep. Add a one-line unlink so existing installs shed the file on next launch.
read_dirof the root, whenbootstrap()already walks the whole tree recursively..txtadded later becomes a note on the next launch, which is what the March feature promised..txta user has deliberately parked in their vault root gets renamed to.mdon next launch instead of being ignored forever. I think that is the intended contract, but it is a change and needs adocs/spec/line..txtfiles are covered.B. Move the sentinel to app-private storage
Keeps once-only semantics and gets the file out of the user's folder.
futo-notes-storeonly knowsroottoday, so this means threading a second app-support path through the store constructor in all three shells (Tauri, iOSNoteVault, AndroidNotesStorage)..txtdead end and the subfolder gap in place, and does nothing for the other six dotfiles.C. Collapse all app-state files under a single hidden
.futo/directoryThis is the fix for the complaint the reporter actually had — internal files sitting among their notes.
.e2ee-state.json. Losing or half-moving that is exactly the failure mode behind the conflict-copy-spam incident, and the sync path filters would need updating alongside it.Suggested scope
Do A now, with a regression test in
crates/futo-notes-store/src/tests.rs, and file C separately.bootstrap_migrates_txt_collisions_once_before_returning_the_snapshot(tests.rs:276) asserts the sentinel is a file attests.rs:288and locks the once-only behavior, so it has to be rewritten as part of A — the new test should assert the sentinel is absent after bootstrap and that a.txtadded after a first bootstrap is migrated by the second.