Fix concurrent-instance memory clobber (last-writer-wins) #85858 - #86618
Fix concurrent-instance memory clobber (last-writer-wins) #85858#86618BananaAccurate wants to merge 1 commit into
Conversation
…h#85858 Make MemoryStore.save_to_disk merge-live + tombstone instead of blind overwrite, so two Hermes instances sharing one profile no longer lose each other's entries or resurrect removed ones. - save_to_disk(merge_live=True) unions live on-disk entries (content-dedup); last-writer-wins becomes last-writer-merges. add opts in. - Tombstone markers (__mem_tomb__:<sha1>) written by remove/replace/ apply_batch for one round, so a stale sibling snapshot skips them instead of resurrecting. Drift guard strips tombstone lines. - Windows lock hardened (msvcrt LK_NBLCK + backoff retry) as a complementary defense. - Adds regression test (two MemoryStore instances sharing one profile). Verified: 40 + 71 in-repo tests pass.
Fix concurrent-instance memory clobber (last-writer-wins) #85858 The merge+tombstone design is a solid answer to the clobber, and the regression tests exercise the real two-instance path. Several issues worth addressing:
|
Fix concurrent-instance memory clobber (last-writer-wins) — #85858
Problem
MemoryStorepersists by rewriting the entireMEMORY.md/user file from anin-memory entry list (
save_to_disk). That list is a snapshot seeded atload_from_disk(), and the backgroundsync_allflush writes it withoutre-reading at flush time. When two Hermes instances share one profile, each
holds its own aged snapshot; whichever
save_to_diskruns last rewrites the filefrom its stale list and silently discards the other instance's concurrent entry.
This is the same class that wiped
MEMORY.mdunder concurrent sessions(reported in #85858).
On Windows the file lock (
msvcrt.locking) is a 1-byte advisory lock that isprocess-dependent and unreliable under OneDrive-synced
AppData, so it does notreliably serialize two processes — making the clobber reachable in practice.
Fix (split into two independently-reviewable patches)
1.
memory_tool_85858_dataloss_fix.patch(required) — merge + tombstoneinstead of blind overwrite:
save_to_disk(..., merge_live=True)unions live on-disk entries the instancedoesn't already hold (content-dedup). Last-writer-wins →
last-writer-merges.
addopts in.__mem_tomb__:<sha1>marker lines):remove/replace/apply_batchwrite a tombstone for the removed/replacedentry for one round, so a sibling holding a stale snapshot skips it
instead of resurrecting it. Tombstones are never surfaced as entries and fade
after one round. The drift guard now strips tombstone lines before its
round-trip check.
addrefuses to re-add a tombstoned entry.2.
memory_tool_85858_lock_hardening.patch(optional hardening) — Windowsmsvcrt.lockingnow uses non-blockingLK_NBLCKwith backoff retry + a 30stimeout, so concurrent writers queue instead of racing and can't deadlock.
This is a complementary defense; the merge/tombstone logic is the real safety
net, so this patch is safe to merge independently (or skip).
Verification
New
tests/tools/test_memory_concurrency_85858.py(3 tests) exercises twoMemoryStoreinstances sharing one profile: concurrentadds both survive;a
removethen a sibling's stale flush does not resurrect the entry; areplacethen a sibling's stale flush keeps the new version and drops the old.tests/tools/test_memory_tool.py: 40 passed.tests/agent/test_memory_*.py+ schema/import tests: 71 passed.Running the real suite caught a bug the standalone model missed: the drift guard
compared the raw file (with the tombstone line) against the tombstone-stripped
round-trip and falsely refused writes; fixed. The standalone model had already
caught three earlier bugs (tombstoning every entry; not filtering stale
in-memory entries; text-vs-marker mismatch in the merge). All fixed.
Standalone model also passes: concurrent adds both preserved;
remove+ stalesibling flush → no resurrection (
['pre','from B']);replaceX→X' + stalesibling flush → old gone, X' kept (
['pre','from B',"secret X'"]).Scope
addusesmerge_live(append is safe).replace/removeusetombstones, not merge, because merging there could resurrect removed content.
guard, so older readers ignore them.
How to apply