Severity: 🟡 Medium (data integrity)
_load_narrative (mneme_narrative.py:45–57) returns ({}, "") on any read/parse error. The caller (_memory_do_update) then treats this as "no narrative yet" and starts fresh, overwriting the truncated file.
If the truncation was caused by a prior crash mid-write (see related: lack of fsync in _save_narrative), this turns a recoverable situation into permanent data loss.
Suggested fix
On read error, quarantine before returning empty:
def _load_narrative(path: Path) -> tuple[dict, str]:
if not path.exists():
return {}, ""
try:
text = path.read_text(encoding="utf-8")
except Exception as exc:
backup = path.with_suffix(f".corrupt-{int(time.time())}.md")
try:
os.replace(path, backup)
print(f"⚠ Mnēmē narrative {path} unreadable ({exc}); moved to {backup}",
file=sys.stderr)
except OSError:
pass
return {}, ""
fm, body = _parse_frontmatter(text)
if not fm and not text.strip():
return {}, ""
if not fm:
return {}, text
return fm, body
Acceptance criteria
- Test: corrupt a narrative file, call
_load_narrative, assert (a) returns empty, (b) file was renamed to *.corrupt-<ts>.md.
Severity: 🟡 Medium (data integrity)
_load_narrative(mneme_narrative.py:45–57) returns({}, "")on any read/parse error. The caller (_memory_do_update) then treats this as "no narrative yet" and starts fresh, overwriting the truncated file.If the truncation was caused by a prior crash mid-write (see related: lack of fsync in
_save_narrative), this turns a recoverable situation into permanent data loss.Suggested fix
On read error, quarantine before returning empty:
Acceptance criteria
_load_narrative, assert (a) returns empty, (b) file was renamed to*.corrupt-<ts>.md.