Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/omind/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 6 additions & 1 deletion src/omind/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
16 changes: 15 additions & 1 deletion src/omind/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
15 changes: 15 additions & 0 deletions tests/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.