Commit4f77b573Recorded18 Jul 2026Repositoryfolio

indexer: skip dotfiles (editor lock/swap) + fault-isolate per-note indexing

Message

A dangling Emacs lock symlink (.#foo.md -> [email protected], created whenever a note is opened in Emacs) matched the .md suffix, entered the note-file walk, and crashed the indexer with io-error when index-note! read the dead symlink — taking down the whole folio MCP server for every client, and leaving any worker that connected during the crash window permanently missing its folio tools.

Two-level fix: - find-md-files-recursive now skips any dotfile (name starting with '.'), cleanly excluding Emacs .#locks, Vim .swp swaps, .DS_Store, etc. Notes are never dotfiles. This is the primary fix (the lock never enters the list). - index-note-safe! wraps per-note indexing in a guard so a single unreadable, dangling, or malformed file logs-and-skips instead of aborting the whole index. One bad file must never be a fleet-wide outage (same class as prior malformed-tags / log crashes).

Test: store-note-files dotfile filtering (prove-fails-first). Version 0.2.6.

Note: the native test backend can't run in a sigil 0.17.16 env (folio pins 0.17.12); filter logic verified via interpreter eval + this test under the matching toolchain at deploy.

Changed
 package.sgl         |  2 +-
 src/folio/index.sgl | 12 ++++++++++--
 src/folio/store.sgl |  6 ++++++
 test/test-note.sgl  | 19 +++++++++++++++++++
 4 files changed, 36 insertions(+), 3 deletions(-)
Diff
package.sglmodified
@@ -6,7 +6,7 @@
6
7
(package
8
name: "folio"
9
version: "0.2.5"
+9
version: "0.2.6"
10
sigil: "^0.17"
11
description: "Markdown-native knowledge and project management via MCP"
12
url: "https://codeberg.org/sigil/folio"
src/folio/index.sglmodified
@@ -172,7 +172,7 @@
172
(prev (assoc name indexed-dict)))
173
(when (or (not prev)
174
(> disk-mtime (cdr prev)))
175
(index-note! idx file-path)
+175
(index-note-safe! idx file-path)
176
(set! reindexed (+ reindexed 1)))))
177
file-dict)
178
(let ((removed 0))
@@ -188,6 +188,14 @@
188
removed: removed
189
total: (length file-dict))))))
190
+191
;; Index one note, isolating per-file failures. A single unreadable,
+192
;; dangling, or malformed file must never abort the whole index — folio
+193
;; serves every client from this one indexer, so one bad entry taking it
+194
;; down is a fleet-wide outage. Log and skip instead.
+195
(define (index-note-safe! idx f)
+196
(guard (exn (#t (log-info "folio: skipping unindexable file" file: f)))
+197
(index-note! idx f)))
+198
199
;;; Drop and rebuild the entire index from scratch.
200
(define (index-rebuild! idx)
201
(: folio-index? -> integer?)
@@ -197,7 +205,7 @@
205
(sqlite-exec db "BEGIN TRANSACTION")
206
(sqlite-exec db "DELETE FROM notes_fts")
207
(sqlite-exec db "DELETE FROM notes_meta")
200
(for-each (lambda (f) (index-note! idx f)) files)
+208
(for-each (lambda (f) (index-note-safe! idx f)) files)
209
(sqlite-exec db "COMMIT")
210
(log-info "Index rebuilt" count: (length files))
211
(length files)))
src/folio/store.sglmodified
@@ -93,6 +93,12 @@
93
(fold-right (lambda (name acc)
94
(let ((full (path-join dir name)))
95
(cond
+96
;; Skip dotfiles entirely: editor lock/swap files
+97
;; (.#foo.md Emacs locks, .foo.md.swp Vim swaps),
+98
;; .DS_Store, etc. Notes are never dotfiles, and a
+99
;; dangling .#lock symlink otherwise matches .md and
+100
;; crashes the indexer when index-note! reads it.
+101
((string-starts-with? name ".") acc)
102
((directory? full)
103
(append (find-md-files-recursive full) acc))
104
((string-ends-with? name ".md")
test/test-note.sglmodified
@@ -60,6 +60,25 @@
60
(assert-true (string-contains? (folio-note-body note) "Some content here."))
61
(assert-equal 2 (length (note-tags note)))))))))
62
+63
;; ============================================================
+64
;; store-note-files skips editor lock/dotfiles
+65
;; ============================================================
+66
+67
(test-group "store-note-files dotfile filtering"
+68
(test "excludes .# Emacs lock files, keeps real notes"
+69
(call-with-temp-directory
+70
(lambda (dir)
+71
(let ((store (make-folio-store dir)))
+72
(note-create! store "Real Note" '("x") "Body.")
+73
;; Simulate an Emacs lock next to the note. It ends in .md, so the
+74
;; pre-fix walk included it, then index-note! crashed reading the
+75
;; (normally dangling) symlink. The walk must filter it by name.
+76
(write-file-string (path-join (store-notes-dir store) ".#real-note.md")
+78
(let ((files (store-note-files store)))
+79
(assert-equal 1 (length files))
+80
(assert-true (string-ends-with? (car files) "real-note.md"))))))))
+81
82
;; ============================================================
83
;; note-edit!
84
;; ============================================================