fix(memory): sign DeerMem reload before reading the document (#6238)

* fix(memory): sign DeerMem reload before reading the document

reload() read the document before computing its cache signature, so a
write committed in between cached the old document under the new
signature and later load() calls served stale memory until the next
write. Compute the signature first, matching load().

* test(memory): cover markdown storage and agent scope in reload race test

* docs(changelog): reference #6238 for the DeerMem reload fix
This commit is contained in:
Hyeonsang Cho 2026-10-04 08:29:56 +09:00 committed by GitHub
parent aaeed26d69
commit e3064f541b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 58 additions and 1 deletions

View File

@ -466,6 +466,13 @@ This release closes that milestone with **301 merged pull requests**.
### Fixed
- **memory:** A DeerMem memory reload no longer pins an older document in the
cache. `reload()` read the document before computing its cache signature, so
a write committed in between (for example by the background memory updater)
cached the old document under the new signature, and every later `load()`
returned the outdated memory until the next write. `reload()` now computes
the signature first, as `load()` already did, so a racing write forces a
re-read instead. ([#6238])
- **channels:** Discord now runs its channel-connection database work on the
Gateway event loop. discord.py delivers messages on a private loop in the
client thread, and the Discord adapter awaited the connection repository there
@ -7679,3 +7686,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
[#6214]: https://github.com/bytedance/deer-flow/pull/6214
[#6229]: https://github.com/bytedance/deer-flow/pull/6229
[#6230]: https://github.com/bytedance/deer-flow/pull/6230
[#6238]: https://github.com/bytedance/deer-flow/pull/6238

View File

@ -423,6 +423,11 @@
### 修复
- **记忆:** DeerMem 记忆重新加载不再把旧文档固定在缓存中。`reload()`
此前先读取文档、后计算缓存签名,若两者之间有写入提交(例如后台记忆更
新器),旧文档就会以新签名写入缓存,之后每次 `load()` 都返回过时的记
忆,直到下一次写入。`reload()` 现在与 `load()` 一样先计算签名,竞争写
入只会触发重新读取。([#6238])
- **渠道:** Discord 的渠道连接数据库操作现在在 Gateway 事件循环上执行。
discord.py 在客户端线程的私有事件循环上投递消息,而 Discord 适配器此前就在该
循环上 await 连接仓库,但仓库的 SQLAlchemy 引擎与连接池属于 Gateway 循环。在
@ -6414,3 +6419,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
[#6214]: https://github.com/bytedance/deer-flow/pull/6214
[#6229]: https://github.com/bytedance/deer-flow/pull/6229
[#6230]: https://github.com/bytedance/deer-flow/pull/6230
[#6238]: https://github.com/bytedance/deer-flow/pull/6238

View File

@ -134,6 +134,7 @@ Use the typed conflict classes instead of matching exception text.
The weak lock cache must not retain inactive user scopes.
Cache validation uses the manifest metadata and persisted revision.
Unlocked `load()`/`reload()` compute that signature before reading the document.
Out-of-band Markdown edits require `reload()`.
POSIX atomic replacement must sync the parent directory.

View File

@ -1396,8 +1396,11 @@ class FileMemoryStorage(MemoryStorage):
migration_notifications = self._run_read_migrations_locked(path, agent_name, user_id=user_id)
for notification_agent, notifications in migration_notifications:
self._dispatch_retrieval_notifications(notifications, user_id=user_id, agent_name=notification_agent)
document = self._read_document(path, agent_name, user_id=user_id)
# Sign before reading, as load() does: a write landing in between then
# leaves a stale signature that forces a re-read, never a stale
# document cached under the new signature.
signature = self._scope_signature(path, agent_name)
document = self._read_document(path, agent_name, user_id=user_id)
with self._cache_lock:
self._memory_cache[key] = (copy.deepcopy(document), signature)
if _rebuild_retrieval and agent_name is not None and self._retrieval is not None:

View File

@ -191,6 +191,45 @@ class TestFileMemoryStorage:
memory2 = storage.reload()
assert memory2["user"]["workContext"]["summary"] == "updated"
@pytest.mark.parametrize("storage_class", [FileMemoryStorage, MarkdownMemoryStorage])
@pytest.mark.parametrize("agent_name", [None, "test-agent"])
def test_reload_does_not_pin_a_document_older_than_its_signature(self, tmp_path, storage_class, agent_name):
"""A write committed while reload() reads must not be hidden from later loads."""
storage = storage_class(DeerMemConfig(storage_path=str(tmp_path)))
def memory_with(text: str) -> dict:
memory = create_empty_memory()
if agent_name is None:
memory["user"]["workContext"]["summary"] = text
else:
memory["facts"] = [{"id": "fact_1", "content": text, "category": "context", "confidence": 0.9}]
return memory
def text_of(memory: dict) -> str:
if agent_name is None:
return memory["user"]["workContext"]["summary"]
return memory["facts"][0]["content"]
assert storage.save(memory_with("old"), agent_name, user_id="alice")
read_document = storage._read_document
raced = False
def read_then_concurrent_write(*args, **kwargs):
nonlocal raced
document = read_document(*args, **kwargs)
if not raced:
raced = True
# A background updater commits after reload() has read the
# document but before it caches the result.
assert storage.save(memory_with("new"), agent_name, user_id="alice")
return document
with patch.object(storage, "_read_document", side_effect=read_then_concurrent_write):
storage.reload(agent_name, user_id="alice")
assert raced
assert text_of(storage.load(agent_name, user_id="alice")) == "new"
class TestCreateStorage:
"""Test create_storage(config) (replaces the old get_memory_storage() singleton)."""