diff --git a/CHANGELOG.md b/CHANGELOG.md index ee64151..d15c40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [8.1.0] - 2026-08-02 + +### Fixed +- **Notes are written with LF on every platform.** `store._atomic_write` and + `paths.atomic_write_text` opened in text mode with the default newline + translation, so the same note was written as LF on POSIX and CRLF on Windows — + and nothing in the code could see the difference, because reads translate + back. **Four bugs came out of that one property**, every one of them passing a + green Linux run and failing only on the Windows legs: the compliance log's + rotation ([#202](https://github.com/CryptoJones/omind/issues/202)), and both + halves of the transaction journal — identity hashes that never matched what + had just been written, and pre-image restores that grew a blank line on each + rollback. + + An explicit `newline="\n"` makes the bytes identical everywhere, which also + makes note digests, mesh merges, and git diffs platform-stable. It matters + most in `paths.atomic_write_text`, which writes `omi-guard.sh` — a shell + script whose shebang ends `\r\n` is not a shell script. Existing CRLF files + keep their bytes until something rewrites them. + ## [8.0.0] - 2026-08-02 ### Added diff --git a/pyproject.toml b/pyproject.toml index ceaab6e..e4e04c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "omind" -version = "8.0.0" +version = "8.1.0" description = "Reproduce the OMI/Obsidian memory integration for AI agents, plus a local web app to view, edit, and add memory entries." readme = "README.md" requires-python = ">=3.10" diff --git a/src/omind/__init__.py b/src/omind/__init__.py index dde076d..2afee3c 100644 --- a/src/omind/__init__.py +++ b/src/omind/__init__.py @@ -2,4 +2,4 @@ # Copyright 2026 Aaron K. Clark """omind — OMI/Obsidian memory tooling for AI agents.""" -__version__ = "8.0.0" +__version__ = "8.1.0" diff --git a/src/omind/paths.py b/src/omind/paths.py index 7ac651f..b3378b1 100644 --- a/src/omind/paths.py +++ b/src/omind/paths.py @@ -53,12 +53,17 @@ def atomic_write_text(path: Path, text: str, *, mode: int | None = None) -> None means every tool call is denied. The temp file + rename makes a concurrent reader see either the old file or the new one in full, and the directory fsync makes the rename itself durable across a power loss. + + ``newline="\n"`` for the same reason as ``store._atomic_write``: the default + rewrites every ``\n`` to ``os.linesep``. That matters more here than there — + this function writes ``omi-guard.sh``, and a shell script whose shebang line + ends ``\r\n`` is not a shell script. """ directory = path.parent directory.mkdir(parents=True, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=directory, prefix=".tmp-", suffix=path.suffix or ".tmp") try: - with os.fdopen(fd, "w", encoding="utf-8") as fh: + with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as fh: fh.write(text) fh.flush() os.fsync(fh.fileno()) diff --git a/src/omind/store.py b/src/omind/store.py index baa8030..72bf65b 100644 --- a/src/omind/store.py +++ b/src/omind/store.py @@ -61,12 +61,26 @@ def _atomic_write(path: Path, text: str) -> None: either the old file or the new one in full — never a half-written file. The parent directory is fsynced after the rename so the rename itself is durable across a power loss, not just the file's data blocks. + + ``newline="\n"`` is load-bearing. The default (``None``) translates every + ``\n`` to ``os.linesep`` on write, so the same note was written as LF on + POSIX and CRLF on Windows — and *nothing in the code could see it*, because + reads translate back. Four bugs came out of that one property: the + compliance log's rotation, and both halves of the transaction journal + (identity hashes that never matched what had just been written, and + pre-image restores that grew a blank line on every rollback). Every one of + them passed a green Linux run and failed only on the Windows legs. + + With an explicit newline the bytes are identical on every OS, which also + makes note digests, mesh merges, and git diffs platform-stable. Existing + CRLF files keep their bytes until something rewrites them; Obsidian, git, + and every parser here read LF fine on Windows. """ directory = path.parent directory.mkdir(parents=True, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=directory, prefix=".tmp-", suffix=".md") try: - with os.fdopen(fd, "w", encoding="utf-8") as fh: + with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as fh: fh.write(text) fh.flush() os.fsync(fh.fileno()) diff --git a/tests/test_store.py b/tests/test_store.py index d663c04..2a8c87d 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -869,3 +869,18 @@ def test_partial_edit_does_not_clear_provenance(tmp_path: Path) -> None: store.update_note("Keep.md", NoteFields(title="Keep", summary="edited")) fields = store.read_fields("Keep.md") assert fields.confidence == "high" and fields.conflicts_with == "[[Other]]" + + +def test_notes_are_written_with_lf_on_every_platform(tmp_path: Path) -> None: + """Byte-identical notes across OSes — the root of four Windows-only bugs. + + `_atomic_write` used the default newline translation, so the same note was + LF on POSIX and CRLF on Windows, and nothing could see it because reads + translate back. That broke compliance-log rotation and both halves of the + transaction journal, each time passing a green Linux run first. + """ + store = OmiStore(tmp_path) + store.create_note(NoteFields(title="Newlines", summary="one", details="two")) + raw = (tmp_path / "Newlines.md").read_bytes() + assert b"\r\n" not in raw + assert b"\n" in raw diff --git a/uv.lock b/uv.lock index fad624c..e994c53 100644 --- a/uv.lock +++ b/uv.lock @@ -2354,7 +2354,7 @@ wheels = [ [[package]] name = "omind" -version = "8.0.0" +version = "8.1.0" source = { editable = "." } dependencies = [ { name = "cryptography" },