fix(memory): log and degrade gracefully on invalid UTF-8 in memory files - #57755
fix(memory): log and degrade gracefully on invalid UTF-8 in memory files#57755JoaoMarcos44 wants to merge 2 commits into
Conversation
_read_file() only caught (OSError, IOError) around read_text(encoding="utf-8"). UnicodeDecodeError is a ValueError subclass, so it escaped that catch, propagated through load_from_disk(), and hit agent_init.py's bare except Exception: pass -- silently disabling memory (both MEMORY.md and USER.md) for the entire session with zero log output. An operator debugging "why did memory stop working" had no signal to go on. Catch UnicodeDecodeError explicitly at the source and log a visible error before returning an empty entry list, so corruption is diagnosable instead of silently swallowed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Related to the memory non-UTF-8 fix cluster for the same bug (#49508 / #10879 family, and the issue this Fixes, #57754). Note the mechanisms differ: #49516 and #10888 recover content (retry with |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the decode failure. The premise is confirmed on current main: tools/memory_tool.py:692 performs a strict UTF-8 read without catching UnicodeDecodeError, and the initialization path swallows that failure at agent/agent_init.py:1389-1391.
Problems
- The same uncaught strict read remains in
_detect_external_drift()attools/memory_tool.py:732._reload_target()calls it attools/memory_tool.py:303beforereplace,remove, andapply_batch, so those mutation paths still fail on invalid bytes. - Returning an empty list can lose the corrupt file's contents.
add()deliberately skips the drift guard attools/memory_tool.py:354, then saves its in-memory list attools/memory_tool.py:382-384; after this fallback that writes over the original invalid bytes. - No regression test is added. Existing persistence/load tests at
tests/tools/test_memory_tool.py:481-503and:843-917cover valid UTF-8 only.
Suggested changes
- Select one data-preserving corrupt-file strategy and apply it to both strict read sites; the existing discussion correctly notes the trade-off between recovery and fail-visible handling.
- Add invalid-byte coverage for initialization and subsequent mutation paths, including protection against overwrite.
Automated hermes-sweeper review.
…clobbering them teknium1's review on NousResearch#57755 pointed out two gaps in the original fix: - _detect_external_drift() still did a strict UTF-8 read, so replace/remove/ apply_batch crashed uncaught on invalid bytes instead of degrading. - add()'s skip_drift=True path treated a corrupt file as empty, then save_to_disk() overwrote it, permanently discarding the original content. Split a target-agnostic _check_decode_corruption() out of _detect_external_drift() and run it unconditionally in _reload_target(), even when skip_drift is set. An undecodable file now backs itself up (raw bytes, since it can't round-trip through the text path) and refuses the mutation via the existing drift_backup/_drift_error contract, the same guard already in place for round-trip drift (NousResearch#26045). Adds regression coverage for the init-time degrade-and-log path plus replace/remove/add refusal and backup-preservation on invalid UTF-8, which the original PR shipped without. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes #57754
Problem
MemoryStore._read_file()(tools/memory_tool.py:691-694) readsMEMORY.md/USER.mdwithpath.read_text(encoding="utf-8")and only catches(OSError, IOError)around it.UnicodeDecodeErroris aValueErrorsubclass, not anOSError/IOError, so invalid UTF-8 bytes in either file raise past this catch. The exception then propagates intoagent_init.py:1239-1253's broadtry/except Exception: pass, which silently swallows it. Net effect: memory is disabled for the whole session (agent._memory_storestaysNone), whileagent._memory_enabled/agent._user_profile_enabledremainTruefrom config — and no log line is ever emitted. There is nothing for an operator to go on when debugging "why did memory stop loading."Fix
Catch
UnicodeDecodeErrorexplicitly in_read_file(), log a clear error (file path + underlying decode error + remediation hint), and degrade to an empty entry list — same graceful-degradation behavior as before, but now visible.This is a minimal, targeted fix at the exact point the exception was escaping — no changes to control flow, call sites, or the broader
except Exception: passinagent_init.py(that catch-all is intentionally permissive by design for other truly-optional memory init steps; narrowing it is a separate, larger change out of scope here).Verification
Reproduced the bug against
mainfirst (confirmed silent failure), then verified the fix on this branch:Test plan
_read_fileraises uncaught)_read_filereturns[]and emits alogger.errorwith file path + decode error