Summary
A wikilink that names a top-level note by its filename stem cannot reach the file-path
fallback in LinkResolver._resolve, because the .md-appending step is gated on the identifier
containing a /. A note in a subdirectory resolves; the same note at the vault root does not.
This matters for vaults edited directly on disk, where the natural way to link is by filename,
and where a note's title: (and therefore its derived permalink) is a descriptive human string
rather than a slug of the filename.
Where
src/basic_memory/services/link_resolver.py on main (401ed6bf), resolution order:
- exact permalink
- exact title
- exact file path —
get_by_file_path(session, clean_text, …)
- file path +
.md — only when not clean_text.endswith(".md") and "/" in clean_text
- fuzzy search (non-strict only)
Two gaps combine:
- (a) Step 4's
"/" in clean_text guard means a bare identifier such as alpha-note is never
retried as alpha-note.md. notes/alpha-note is. There is nothing about a root-level note that
makes the .md retry less correct — the guard appears to be guarding against something else.
- (b) No
_↔- normalization anywhere in steps 3–4, so alpha_note.md on disk is
unreachable from [[alpha-note]] even if the .md were appended.
Reproduce
mkdir -p /tmp/bm-repro && cd /tmp/bm-repro
cat > alpha_note.md <<'EOF'
---
title: Alpha — a descriptive human title
type: note
---
Body.
EOF
cat > beta_note.md <<'EOF'
---
title: Beta
type: note
---
Links to [[alpha-note]] and [[alpha_note]].
EOF
basic-memory project add repro /tmp/bm-repro
basic-memory sync -p repro
Expected: both links resolve to alpha_note.md.
Observed: the relations stay unresolved.
Step 1 misses because the permalink derives from the descriptive title, not the filename. Step 2
misses for the same reason. Step 3 misses because the stored file_path is alpha_note.md, not
alpha-note. Step 4 is skipped — no /.
Moving the same note to notes/alpha_note.md and linking [[notes/alpha-note]] still fails on
(b); linking [[notes/alpha_note]] succeeds via step 4, which is the asymmetry this report is
about.
Impact
In a ~350-relation vault where notes carry descriptive titles and links are written as filenames,
this left the great majority of relations unresolved. sync reports success and nothing is
logged, so the graph looks healthy while having almost no edges. The failure is only visible if
you go looking for it.
Suggested fix
Drop the "/" condition on step 4 — retry f"{clean_text}.md" for bare identifiers too — and
normalize _↔- (and case) when comparing against stored file_path. Both are additive: they
only turn current misses into hits, and step 4 already runs after the exact permalink/title/path
attempts, so nothing that resolves today changes.
If strict-by-design is preferred over either change, then emitting one warning per unresolved
link at sync time would at least make the failure visible.
Verification
Symptom reproduced on 0.22.1 (current PyPI release, macOS 15, Python 3.14, local file
backend). The resolution order and the "/" guard above were read from main at 401ed6bf;
I have not run main, so the exact step numbering there is from source, not from a live trace.
Filed separately from #1015 (forward-reference back-resolution), which the v0.23 change review
lists as fixed by #1161/#1159 — that is a different failure and I am not re-reporting it.
Summary
A wikilink that names a top-level note by its filename stem cannot reach the file-path
fallback in
LinkResolver._resolve, because the.md-appending step is gated on the identifiercontaining a
/. A note in a subdirectory resolves; the same note at the vault root does not.This matters for vaults edited directly on disk, where the natural way to link is by filename,
and where a note's
title:(and therefore its derived permalink) is a descriptive human stringrather than a slug of the filename.
Where
src/basic_memory/services/link_resolver.pyonmain(401ed6bf), resolution order:get_by_file_path(session, clean_text, …).md— only whennot clean_text.endswith(".md") and "/" in clean_textTwo gaps combine:
"/" in clean_textguard means a bare identifier such asalpha-noteis neverretried as
alpha-note.md.notes/alpha-noteis. There is nothing about a root-level note thatmakes the
.mdretry less correct — the guard appears to be guarding against something else._↔-normalization anywhere in steps 3–4, soalpha_note.mdon disk isunreachable from
[[alpha-note]]even if the.mdwere appended.Reproduce
Expected: both links resolve to
alpha_note.md.Observed: the relations stay unresolved.
Step 1 misses because the permalink derives from the descriptive title, not the filename. Step 2
misses for the same reason. Step 3 misses because the stored
file_pathisalpha_note.md, notalpha-note. Step 4 is skipped — no/.Moving the same note to
notes/alpha_note.mdand linking[[notes/alpha-note]]still fails on(b); linking
[[notes/alpha_note]]succeeds via step 4, which is the asymmetry this report isabout.
Impact
In a ~350-relation vault where notes carry descriptive titles and links are written as filenames,
this left the great majority of relations unresolved.
syncreports success and nothing islogged, so the graph looks healthy while having almost no edges. The failure is only visible if
you go looking for it.
Suggested fix
Drop the
"/"condition on step 4 — retryf"{clean_text}.md"for bare identifiers too — andnormalize
_↔-(and case) when comparing against storedfile_path. Both are additive: theyonly turn current misses into hits, and step 4 already runs after the exact permalink/title/path
attempts, so nothing that resolves today changes.
If strict-by-design is preferred over either change, then emitting one warning per unresolved
link at sync time would at least make the failure visible.
Verification
Symptom reproduced on 0.22.1 (current PyPI release, macOS 15, Python 3.14, local file
backend). The resolution order and the
"/"guard above were read frommainat401ed6bf;I have not run
main, so the exact step numbering there is from source, not from a live trace.Filed separately from #1015 (forward-reference back-resolution), which the v0.23 change review
lists as fixed by #1161/#1159 — that is a different failure and I am not re-reporting it.