Repository navigation
fix: read and edit notes with Windows line endings without a stray carriage return - #680
Conversation
…ut a carriage return gray-matter closes the properties block at `\n---`, so in a note with Windows line endings the block kept the `\r` of its last line. The YAML parser read it into the last value (`title: "C\r"`), which every rewrite then saved, or refused the whole block when that value was quoted or a bracketed list. splitPropertiesBlock now drops that `\r`, so the block reads as the same note with LF endings does. Note bodies keep their bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's LF The edit tools match against a note body split into LF lines, so old_text copied from a raw read of a CRLF note never matched, and \r\n in new_text or content was written byte for byte into LF output. replace_in_note now converts old_text and new_text, and patch, replace_span and insert_at_anchor split content with splitIntoLines, which also lets the leading-heading check drop its own carriage-return strip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… LF line endings The description said unpaged reads stay byte-identical, but an unpaged heading read of a note with Windows line endings returns LF lines. It now limits the byte-for-byte promise to unpaged full reads, and drops "verbatim" from the heading-mode pointer so the two statements agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- frontmatter.ts: say why only the last line's carriage return is dropped, what the block text keeps, why line numbers match the note, and that replacing a CRLF note's block leaves mixed line endings. - vault-patcher.ts: comment each content split's CRLF conversion, the old_text conversion and its empty-string guard, the anchor and insert exceptions, and the LF-only offsets in listTextDeletionEdits; one spelling for empty-string guards; firstNonBlankLineIndex. - vault_read_note: name what full reads match, move the line-ending sentence to Returns, and say "Kanban board". Snapshot regenerated. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s docstring Obsidian's Properties panel rewrites the whole note with LF; only its processFrontMatter API leaves mixed line endings. Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot-found quote - updateProperties on a CRLF note keeps the last property without a carriage return and the body's CRLF bytes - readNoteSection returns a CRLF note's section with LF line breaks, as the vault_read_note description now states - replaceInNote's not-found message quotes old_text as sent, CRLF included - the CRLF old_text test sends an LF new_text, so it fails only when old_text matching breaks Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… line The opener may carry trailing spaces or tabs, which stay at the start of the block text, so the text does not always start with the line break. Line numbers still match the note's. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roperties result
The tool-definition review of this branch's vault_read_note edit found
gaps the tool already had:
- ENAMETOOLONG and EACCES (or another filesystem error code) reach the
client, but no Errors entry named them; tool-helpers says each tool
lists these when its description is next revised.
- properties_only returns {} for a note with no properties.
- The one-mode-per-call rule sat under "Section boundaries".
- The ".md" entry lost its remedy when vault_read_file is off.
The per-tool size allowance rises from 4,226 to 4,234 to fit them.
Ship-Check: tool-definitions · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Drop the stale task_guide_test_anchor
Pre-existing: vault_list_tasks' routing sentence still promises heading mode returns a byte-exact lane text, but readNoteSection normalizes a CRLF note's lines to LF, so the returned section is not byte-verbatim. This PR dropped the same word from vault_read_note's own routing sentence and its Returns section now states heading sections use LF line endings, leaving the sibling claim contradicting the tool it points at — for exactly the Windows-line-ending notes this PR targets. Failure scenario: On a vault with CRLF notes (the scenario this PR fixes elsewhere), an agent following vault_list_tasks' routing reads a Kanban lane with vault_read_note's heading mode and receives LF-normalized text whose bytes differ from the note's CRLF bytes — the 'verbatim' promise is broken, and the same tool surface now says elsewhere that heading sections use LF line endings. Suggested fixIn task-tools.ts, trim the sentence to "...only when you need a lane's Markdown or a task's state right after a write." to mirror vault_read_note's routing sentence, and update the pinned expectation in this test — it can ride the next vault_list_tasks description revision per the tool-by-tool editing rule.umm-actually · z-ai/glm-5.3-flash |
|
umm-actually reviewed at 1 new finding(s) posted (1 tracked finding(s) across all runs). Context notes
umm-actually · z-ai/glm-5.3-flash |
|
Re: issue comment 6222861364 ( Valid. Heading mode has always returned LF lines for a note with Windows line endings, and The maintainer approved keeping 🔍 ship-check · pr-monitor · claude-opus-5-5 |
Summary
Notes saved with Windows line endings (CRLF), common in vaults checked out on Windows through git, now read and edit cleanly.
Before this change:
title: Cread as"C\r", an emptydue:read as"\r"instead of empty, and every edit saved the value back astitle: "C\r".title: "C",tags: [a, b], or arelated:list of quoted links, the form Obsidian writes, was refused as "properties block is not valid YAML". Property reads, outline and section reads, every edit tool, task and memory tools, and moves all failed on such a note.old_textwith\r\nreturned "text not found", and\r\ninnew_textorcontentwas written byte for byte into notes the server otherwise writes with LF.Changes
frontmatter.ts): the split closes the block at\n---, which left the\rof the last line's break at the end of the block. That\ris now dropped, so the block reads as the same note with LF endings does, which matches Obsidian's metadata cache. Note bodies keep their bytes, sovault_update_propertiesstill keeps the body as it says.vault-patcher.ts):vault_replace_in_noteconverts\r\ninold_textandnew_textto\n.vault_patch_note,vault_replace_spanandvault_insert_at_anchorsplitcontentthe way the note body is split, so its Windows line breaks are written as LF and its blank lines count as empty lines. The leading-heading check no longer needs its own carriage-return strip.vault_read_notedescription: only an unpaged full read returns the file's exact bytes; heading sections and paged reads use LF line endings (true before this change too; the old sentence said every unpaged read was byte-identical). It also lists two errors the tool already returns (ENAMETOOLONG,EACCESand other filesystem error codes), saysproperties_onlyreturns{}for a note with no properties, and moves the one-mode-per-call rule out of the "Section boundaries" paragraph. Snapshots regenerated.Upgrade notes
"C\r"). This change stops new ones; an existing one stays until it is rewritten. Fix an affected value withvault_update_properties(for example{ "title": "C" }).Not in this change
|+block, for example) loses those trailing line breaks on any rewrite. This is a pre-existing loss on LF notes too; on CRLF notes such a value was refused before and now behaves as on LF notes.---still split differently in the task tools.toContain) assertions in the three edited test files are left for a separate test-only change.Tool definitions
Checked with the tool-definition reviewer against main's tool list, in every server configuration:
vault_read_noteis the only tool whose text changed; no other tool, server instruction or prompt changed.ENAMETOOLONGandEACCES(or another filesystem error code). The code notes that each tool lists these when its description is next revised, so they are added here, with{}for a note with no properties.Size:
vault_read_note5,735 → 5,990 characters; the default tool list 139,437 → 139,692 (+255).CHARS_PER_TOOL_ALLOWANCErises from 4,226 to 4,234 to fit the two error entries.Test plan
npm test: 116 files, 5,317 passed, 2 skippednpm run lint,npm run knip,npm run build(both typechecks)Live Test Results
Head: f786fda
org.opencontainers.image.revision=f786fdad5588b9549500f19d49d4dc38acbb8e52; the log shows the container Recreated, Started and Healthy. On the host, the running container isghcr.io/aliasunder/vault-cortex:test, imagesha256:a68479ec8adc…, revision labelf786fdad5588b9549500f19d49d4dc38acbb8e52, started 2026-10-16 19:26:40 UTC.ghcr.io/aliasunder/vault-cortex:remotev0.54.14, imagesha256:d8c09778d068…, revision labelaa361ed4540872cc997ed0c0416210f61a867f5b./mcpendpoint, so real carriage returns reach the server (a typed\r\nin a chat client arrives as four characters). Every probe wrote its own note per image, and a raw read confirmed the CRLF bytes before each operation. All probe notes were deleted afterwards.vault_write_notewith a CRLF body ending its block intitle: C, thenproperties_only{"title": "C\r"}{"title": "C"}related:list of a quoted wikilink as the last propertyproperties_onlyreturns{"title": "C", "related": ["[[Scratch/crlf-live-test/target]]"]}vault_replace_in_notewithold_text"- [ ] task\r\n"copied from a raw read---\ntitle: C\n---\n# H\nA\n- [x] task\nB\n, properties still{"title": "C"}vault_patch_noteappend with content"X1\r\nX2"X1\r\nX2\n, block saved astitle: "C\r"X1\nX2\n, no carriage return anywhereHreturns# H\nA\nB, bytes 7 (the LF section, not the CRLF one)🤖 Generated with Claude Code