Repository navigation
fix(tools): save through a short hidden temporary file and stop memory names ending in .md - #710
Conversation
A save staged its content in `<note>.<uuid>.tmp` beside the note, so a note named within 41 bytes of the filesystem's limit could be read but not saved, and Obsidian Sync logged an error for each save when it opened the temp file after the rename had moved it. The temp file is now `.<uuid>.tmp` in the note's folder: 41 bytes whatever the note's name, and hidden, so Sync and the file watcher skip it. The temp-name remedy and its tool-description text go, along with the tool-availability argument only that remedy used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…entry dates - The memory tools add .md to the file name, so `file: "Opinions.md"` created `Opinions.md.md` beside `Opinions.md`. A name ending in .md, in any letter case, is now refused before anything is read or written. - vault_delete_memory refused an entry whose date a hand edit made impossible (2026-02-30), so only a manual edit could remove it. It now checks the date's shape only; vault_update_memory still refuses an impossible date for a new entry. - When a file blocks the memory folder, the message names the folder on the way that is not a folder, so a nested MEMORY_DIR points at the right path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…date ARCHITECTURE.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Make vault_list_memory_files skip names the .md refusal now rejects
The PR adds a refusal for memory file names ending in .md, but the lister still derives a tool-usable name from any visible .md file via basename(filename, '.md'), so a doubled-extension file (e.g. one created by the old bug this PR fixes, or by hand) is listed as file: 'Opinions.md'. The listing's own description promises 'file is the name the other memory tools take as file (no .md)', and every tool that name feeds now rejects it, leaving the listed file operable through no memory tool and only discoverable through the refusal error. Failure scenario: A vault holds About Me/Opinions.md.md left over from the pre-fix bug (file: 'Opinions.md' created it). vault_list_memory_files returns file: 'Opinions.md'; the caller passes that exact name to vault_get_memory, vault_update_memory, or vault_delete_memory and gets 'memory file must be named without .md: “Opinions.md”' on every call — the file the listing recommended is unusable through any memory tool. Suggested fixIn listVisibleMemoryFilenames (or the outline/name mappers), skip names whose basename-minus-.md still ends in .md (case-insensitive), so the listing only offers names the tools accept — or surface such files with a flag the descriptions can document.umm-actually · z-ai/glm-5.3-flash |
|
umm-actually re-reviewed at 1 new finding(s) posted (8 tracked finding(s) across all runs). Context notes
umm-actually · z-ai/glm-5.3-flash |
- Correct the atomicWriteFileExclusive cleanup comment for the link path - Name where the watcher skips hidden paths and show the 41-byte sum - Rename findMemoryFolderBlocker's stats local and fix its docstring - Say why mkdir's EEXIST/ENOTDIR are wrapped and which case gives which - Name the files behind the coded-error remedies' race assumptions - Extract a named local in exceedsCharCap and explain capContent's guard - Align ARCHITECTURE.md's temp-file cleanup with both write paths Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…folder listings after saves - memory-store: a nested MEMORY_DIR whose parent is a folder and whose own path is a file names "Meta/About Me" as the blocker, so the check must walk past the first segment - vault-filesystem: the no-staging-file-left tests assert the folder's exact listing instead of filtering for names ending in .tmp, which would pass if the temp name stopped ending that way Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p the stale path key from the remedy docs A broken symlink at the memory folder's path made mkdir fail with ENOENT, which reached the client with a retry remedy that cannot fix it. It is now wrapped like a file at the folder's path, so the memory tools' owner-fix entry applies. ARCHITECTURE.md said the filesystem remedy is keyed on the error's path; it is keyed on the code and syscall only. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ack's remedy join - vault-filesystem.ts wrote the same log-don't-throw cleanup four times; removeLeftoverFile holds that rule once. - appendCodedErrorRemedy had one caller and only joined two calls; prompt-helpers.ts now joins them as tool-helpers.ts does. Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ory store memory-store.ts kept its own copy of the entry-line regex, and the delete guard added a third for the date's shape. memory-entries.ts now exports isMemoryEntryLine; the store's entry scan and the delete guard (testing the bullet it builds) both use it, so they agree with the parser by construction. Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tings and recall A file like Opinions.md.md, which the old name handling created, listed as file "Opinions.md", a name every memory tool now refuses, though the listing promises its names are ones the other memory tools take. The memory listings, the all-files read and recall now leave such files out; vault_read_note still reads them. Ship-Check: pr-monitor · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the PR-level finding "Make vault_list_memory_files skip names the .md refusal now rejects" (umm-actually, comment 6391962464): Fixed in d06030e. A file whose name ends in 🔍 ship-check · pr-monitor · claude-opus-5-5 |
|
Update vault_list_memory_files' Behavior text for the doubled-name exclusion
The description still promises 'Lists every .md file directly inside' the memory folder, but this PR's listing filter now also drops files whose name ends in .md twice, so a legacy doubled file is silently absent from the enumeration the text states. The PR description carries the same stale claim ('still shows in memory listings as Opinions.md'), so both surfaces now disagree with the shipped behavior the tests pin. Failure scenario: A vault holds About Me/Opinions.md.md left over from the pre-fix bug this PR describes. An agent reading vault_list_memory_files' description trusts 'Lists every .md file directly inside About Me/' to enumerate the folder, never learns the doubled file exists, and a cleanup pass built on that enumeration never touches it — the file's only discoverable trace is a refusal error if something happens to pass file: 'Opinions.md'. Suggested fixAmend the Behavior sentence to state the second exclusion, e.g.: 'Behavior: Lists every .md file directly inside About Me/, sorted by file name, whether or not it has frontmatter or a scope callout. Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice (which no memory tool can address) are skipped.'umm-actually · z-ai/glm-5.3-flash |
Ship-Check: pr-monitor · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the PR-level finding "Update vault_list_memory_files' Behavior text for the doubled-name exclusion" (umm-actually, comment 6393517297): Fixed in abc25ee. The Behavior sentence now reads "Subfolders are not read, and hidden (dot-prefixed) files and names ending in .md twice are skipped." The PR description states the same. 🔍 ship-check · pr-monitor · claude-opus-5-5 |
The delete guard tested the bullet it builds against the entry pattern, which is not anchored after the date, so a date such as "2026-07-02**: x" passed and could delete an entry whose text held "**:". The guard now requires the date the parser reads from that bullet to equal the date passed. memoryEntryLineDate returns that date, and the parser and isMemoryEntryLine use it too. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck, untangle the delete date comment - removeLeftoverFile's logPath read as the path of a log file; it is the path the warning names, now loggedPath. - memoryFileNameFromPath passed a whole path to isDoubledMarkdownFileName, which takes a file name; it now passes the basename (same result, since the suffix cannot span a slash). - deleteMemory's guard comment listed a passing case under a 'refused' header; the two refused cases stay bullets and the passing case is its own sentence. A blank line now separates the guard from the locked section. - isMemoryEntryLine's doc repeated the shape-only caveat stated two lines above. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e entry line's colon - memory-index: a rebuild removes entry rows an earlier index held under file "Opinions.md" for About Me/Opinions.md.md, the only path that stops recall offering that name once a deployment upgrades. - memory-entries: isMemoryEntryLine refuses a bold date with no colon after it; no other test pinned the pattern's colon. Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It removed no Errors text and prevented no failure: it sharpened a message for a rare, non-default nested MEMORY_DIR, and for the default folder it only repeated the folder's name. The message is as on main again; the broken-symlink wrap stays. Ship-Check: design-pass · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The permission test's comment said only a file at the folder's path is wrapped. The store also wraps a file above the folder and a broken symlink at its path. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Wrap bootstrapMemoryDir's mkdir failure like updateMemory's
Pre-existing: bootstrapMemoryDir creates the memory folder with a bare mkdir, so a MEMORY_DIR that is a dangling symlink throws a raw ENOENT out of server startup — while this PR's own fix routes the identical state through the wrapped, owner-facing 'cannot create memory folder' error on tool writes, and reads already degrade gracefully (the folder listing returns empty). The boot path is the one surface left treating an owner-fixable memory-folder obstruction as a raw failure. Failure scenario: MEMORY_DIR points at a symlink whose target was deleted (e.g. the linked folder was moved). On every container start, bootstrapMemoryDir's access() sees ENOENT and falls through to mkdir(dirPath, { recursive: true }), which throws the raw ENOENT the PR's own new test shows this exact call produces for a dangling symlink; startup fails or logs the container's absolute path instead of the owner-fix message the tool path now gives, and the restart policy loops on a state nothing in the boot output explains. Suggested fixApply the same folderPathBlocked wrap (EEXIST/ENOTDIR/ENOENT → 'cannot create memory folder "…"' with the cause logged) around bootstrapMemoryDir's mkdir, mirroring updateMemory's.umm-actually · z-ai/glm-5.3-flash |
|
Wrap bootstrapMemoryDir's mkdir failure like updateMemory's (comment 6394424161) The maintainer decided not to change this. When 🔍 ship-check · pr-monitor · claude-opus-5-5 |
Summary
Saves now stage their content in a short hidden temporary file, and the memory tools refuse a file name ending in
.md.Hidden temporary file. A save wrote
<note>.<uuid>.tmpbeside the note, then renamed it over the note. Two problems came from that name:ENOENTerror whenever it opened the temporary file after the rename had moved it, which happened on about one save in four.The temporary file is now
.<uuid>.tmpin the note's folder. Its name is always 41 bytes, and it is hidden, so Sync and the file watcher skip it. It stays in the note's folder, so the rename or hard link stays on one filesystem. The temporary-name remedy and its tool-description advice are removed.Memory file names ending in
.md. The memory tools add.mdthemselves, sofile: "Opinions.md"createdOpinions.md.mdbesideOpinions.md.vault_get_memory,vault_update_memoryandvault_delete_memorynow refuse such a name in any letter case:memory file must be named without .md: "Opinions.md". A file likeOpinions.md.mdthat the old behaviour created is left out ofvault_list_memory_files, the memory-review autocomplete, the all-filesvault_get_memoryread and recall, since no memory tool takes the nameOpinions.md.vault_read_notestill reads it, and renaming it in Obsidian makes it a memory file again.Hand-edited entry dates.
vault_delete_memoryrefused an entry whose date a hand edit made impossible (2026-02-30), so only a manual edit could remove it. It now checks only the date's shape (date must be YYYY-MM-DD (e.g. 2026-07-02)).vault_update_memorystill refuses an impossible date for a new entry.Broken memory-folder symlink. A
MEMORY_DIRthat is a symlink pointing nowhere now gets thecannot create memory foldermessage, whose Errors entry sends the vault's owner to fix it, instead of a rawENOENTwith a "retry" remedy that can never succeed.Tool text
vault_move_notevault_update_memory.mdvault_delete_memoryvault_get_memory.mdvault_list_memory_files.md.mdnamesThe default tool list goes from 136,873 to 136,639 characters, and its
Errors:text from 34,471 to 34,207.CHARS_PER_TOOL_ALLOWANCEstays at 4,198.Tool-definition review: run on the four first-round tools in the default list and on
vault_move_notein thedisabled-toolslist, then re-run onvault_list_memory_filesafter its Behavior change. No round found a defect in the text this change edits; the cold reads listed gaps elsewhere in these tools for a later description pass.Tests
.mdrefusal on all three memory tools, and.md.mdfiles left out of the listings, the all-files read and recall;npm run lint,npm run build:serverandnpm run knippass.npx vitest run src/vault-mcp src/__tests__/integration src/utils scripts/__tests__passes, apart from macOS watcher timing tests that also fail onmainunder the same full local run.Live Test Results
Local images with a scratch vault,
MEMORY_DIR="Meta/About Me"and no Sync. Before isghcr.io/aliasunder/vault-cortex:0.55.3(revision8e2ce992ed143476f9957197e90fc21c0c2a0f63). After is this branch built with--target localat390a7bd9.vault_update_memoryfile: "Probe.md"Added entry to Meta/About Me/Probe.md.md; file createdmemory file must be named without .md: "Probe.md"; nothing writtenvault_get_memoryfile: "Probe.md"Probe.md.mdvault_delete_memoryon a2026-02-30entrydate must be a real ISO calendar date …Deleted entry from Meta/About Me/Hand.md → ## Notesvault_delete_memorydate: "2026-1-1"date must be a real ISO calendar date …date must be YYYY-MM-DD (e.g. 2026-07-02)vault_update_memoryon a memory file with a 254-byte nameUnknown system error -115on Docker Desktop's bind mount)vault_write_noteto a 254-byte namevault_move_noteto a 254-byte namemove aborted: could not write to …No
.tmpfile was left in either vault, and neither container restarted.Sync check against real Obsidian Sync (production, the same window length counted from each container's start):
8e2ce992)9a690497)ENOENT … .tmpSync errorsThe test build was confirmed by the container's image revision label and by symbols only this branch has in its
dist/. The fixtures were deleted afterwards. The commits after9a690497change memory listings, the all-files read, recall and the delete guard's internals, which the unit and integration tests cover, plus comments and docs.This PR is releasable on its own.
🤖 Generated with Claude Code