fix(memory): tolerate non-UTF-8 bytes in USER.md/MEMORY.md reads - #54044
fix(memory): tolerate non-UTF-8 bytes in USER.md/MEMORY.md reads#54044Bartok9 wants to merge 2 commits into
Conversation
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Fixes UnicodeDecodeError when reading USER.md/MEMORY.md files with stray non-UTF-8 bytes (e.g. smart quotes saved under cp1252 mismatch). Changes read_text(encoding='utf-8') to read_text(encoding='utf-8', errors='replace') in both _read_file and _detect_external_drift. The errors='replace' mode substitutes replacement characters for invalid bytes, preventing the error from wedging every future memory save. Clean single-line fix with two regression tests.
Reviewed by Hermes Agent
|
Thanks for mapping the cluster. Agreed this isn't a clean duplicate — #53833 is the specific issue this fixes, while #10888 and #18157 tackle the broader encoding family via different mechanisms. This PR is intentionally narrow (USER.md/MEMORY.md reads only). Happy to coordinate consolidation if a maintainer wants the wider robustness pass unified, but this one is safe to land standalone. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the narrowly scoped robustness fix. Live main still strictly decodes both relevant memory reads at tools/memory_tool.py:692 and tools/memory_tool.py:732, so the premise remains valid.
Problems
tests/tools/test_memory_tool.py:796-812verifies loading but does not exercise the changed drift-check route used byreplace/remove. It also does not assert the promised replacement-character behavior;assert len(entries) == 2alone would also pass if bad bytes were silently ignored.
Suggested changes
- Assert that the invalid byte decodes to
\uFFFD. - Add a malformed-file mutation test that reaches
_reload_target/_detect_external_driftand verifies areplaceorremovecompletes successfully.
Automated hermes-sweeper review.
| p.write_bytes(b"Daniel prefers tables\n\xc2\xa7\nLikes \xd1 punchy prose") | ||
| entries = MemoryStore._read_file(p) | ||
| assert any("Daniel prefers tables" in e for e in entries) | ||
| assert len(entries) == 2 |
There was a problem hiding this comment.
This only proves decoding did not raise and the delimiter still split the file; errors="ignore" would also satisfy it. Please assert that the second entry contains \uFFFD so the regression protects the requested replacement semantics.
Address hermes-sweeper review on NousResearch#54044: - assert invalid bytes become U+FFFD (not silently ignored) - cover replace/remove via _reload_target/_detect_external_drift so the drift-check read path is exercised with malformed files
2e12744 to
24dd357
Compare
|
Thanks @teknium1 — both review items addressed on the latest push (rebased onto current
Local: |
|
Closing as redundant with credit: current main's |
Closes #53833.
Root Cause
Symptom — When
USER.mdorMEMORY.mdcontains a byte outside valid UTF-8 (e.g. a smart-quote/dash saved under a cp1252 mismatch), every subsequent memory save crashes withUnicodeDecodeError: 'utf-8' codec can't decode byte 0xd1 ... invalid continuation byte, permanently disabling memory writes for that profile (reporter also saw cascading send-pipeline failures in the same broken session).Root cause —
MemoryStore._read_file()(and_detect_external_drift()) read the file withpath.read_text(encoding="utf-8")and only catch(OSError, IOError). AUnicodeDecodeErroris aValueError, not anOSError, so it escapes the guard and propagates up through_reload_target→add→memory_tool.Evidence —
tools/memory_tool.py:639and:683. New regression testtests/tools/test_memory_tool.py::TestNonUtf8Bytes::test_read_file_replaces_invalid_bytesreproduces the exact traceback on currentmain:Fix + why this level — Add
errors="replace"to both reads, matching the issue's suggested fix. This is the correct layer: the file is already on disk with bad bytes, so the read must be made resilient (catching the exception elsewhere would lose the content entirely and still drop the entry).errors="replace"substitutes the standard U+FFFD replacement char and preserves all surrounding valid content, so memory keeps working.Scope / risk — Touches only the two read paths in
memory_tool.py. Valid UTF-8 files are byte-identical (no behavior change); only previously-crashing files now read with replacement chars. Writes already go through atomic rename and re-serialize as UTF-8, so a load→save cycle naturally heals the bad bytes.Verification
python -m pytest tests/tools/test_memory_tool.py— 78 passedTestNonUtf8Bytes(2 tests):test_read_file_replaces_invalid_bytes,test_load_from_disk_survives_invalid_bytes. Both FAIL without the fix (UnicodeDecodeError) and pass with it.Real behavior proof
After fix, running the new regression tests on this branch: