Skip to content

fix(memory): rebuild curated memory from all ledgers when MEMORY.md is blank - #3091

Open
wzq-xzwj wants to merge 1 commit into
agentscope-ai:mainfrom
wzq-xzwj:fix/memory-consolidator-blank-view-rebuild
Open

fix(memory): rebuild curated memory from all ledgers when MEMORY.md is blank#3091
wzq-xzwj wants to merge 1 commit into
agentscope-ai:mainfrom
wzq-xzwj:fix/memory-consolidator-blank-view-rebuild

Conversation

@wzq-xzwj

Copy link
Copy Markdown
Contributor

Closes #3090

问题

MemoryConsolidator 每次合并的输入组装是"当前 MEMORY.md(或 (empty))+ 水位之后严格修改的账本",用模型输出覆写 MEMORY.md,然后推进水位。它从不校验 MEMORY.md 是否承载着此前合并保留下来的长期记忆。

当 MEMORY.md 缺失或为空而水位文件还在时——手动清理、误删等——下一次合并会把"仅剩的水位后账本"合并进空视图并覆写文件:水位之前未再次修改的旧账本不参与重建,整个过程没有针对该状态的告警。

修复

consolidate() 中:策划视图空白而水位已存在时,记一条警告并把本次运行的水位按 EPOCH 处理——与首次运行相同的输入组装。(since ...) 提示词标签与账本过滤共用同一个变量,自动消失。Javadoc 已补充说明重建行为。

各状态下的行为对照:

状态 修复前 修复后
视图存在,有新账本 增量合并 不变
视图存在,无新账本 跳过 不变
无水位(首次运行) 全部账本 不变
视图缺失或为空,水位存在 把水位后账本合并进 (empty)——旧账本不参与 警告 + 从全部可用账本重建

修复后的语义:当 MEMORY.md 缺失或为空且已有合并水位时,本次合并重新纳入全部可用账本。这会重新生成记忆摘要,不保证还原原文件;人工补充过、或已无对应账本的内容可能无法恢复。

限制

  • 读取错误无法与空文件区分:MEMORY.md 的瞬时读取失败同样会触发重建路径(见 issue 的"相关问题")。
  • 部分账本读取失败尚未阻止合并继续执行。
  • 全量输入可能超出模型上下文限制:consolidation 提示词已带输出侧的 token 预算约束,但输入侧没有截断。

测试

  • MemoryConsolidatorFilesystemTest 9 项全部通过,其中新增 4 项:
    • consolidate_rebuildsFromAllLedgersWhenMemoryMdBlank——水位前、水位后的账本都进入模型输入,无 (since ...) 标签,MEMORY.md 写入模型输出,水位正常推进
    • consolidate_rebuildsEvenWithoutFreshLedgersWhenMemoryMdBlank——水位后没有新提取也能重建丢失的视图(修复前会跳过,旧账本永远不参与)
    • consolidate_keepsIncrementalMergeWhenMemoryMdPresent——增量合并行为不变
    • consolidate_skipsWhenViewPresentAndNoFreshLedgers——跳过行为不变
  • agentscope-harness 模块全量 2335 项:除 2 项既有错误外全部通过(DangerousPathBypassTest 的 2 个 symlink 用例需要 Windows 管理员特权,属环境限制,与本改动无关)。
  • 等效的重建语义已在下游部署中完成端到端验证:删除 MEMORY.md 后,下一次合并自动从全部账本重建。

开放问题

全量重建会把整个账本历史在一次调用中发给 LLM,token 开销高于增量合并;替代方案是检测到该状态时跳过并告警。选用重建的理由:每日账本是事实源头、MEMORY.md 是派生视图,目前对丢失的视图没有任何恢复机制;代价是重建调用更重,且输出是重新生成的摘要而非原文件还原。如果维护者倾向更保守的行为,可以随时调整。

…s blank

A blank curated MEMORY.md with an advanced consolidation watermark means
the view was lost or cleared without resetting the watermark: merging
only post-watermark ledgers into "(empty)" silently drops every older
day from the curated file, and nothing rebuilds it afterwards (the
daily ledgers still hold the facts). readMemoryMd also returns "" on
transient read errors, so this is not limited to manual deletion.

Treat a blank view with an existing watermark as a lost view: warn and
assemble the consolidation input like a first run (all ledgers, no
"(since ...)" label). Consistent states keep their exact previous
behavior. Covered by four new cases in MemoryConsolidatorFilesystemTest.

@dailingtao dailingtao left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

seedStoreFile(
store,
ns,
"memory/2025-06-14.md",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: could we also cover an archived ledger here (for example, memory/archive/2025-06-14.md)? Memory maintenance normally moves old daily ledgers under memory/archive/ after the retention period, and the recovery guarantee depends on the filesystem glob remaining recursive. A regression test would make the “all available ledgers” contract explicit.

@CryoThrust CryoThrust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rebuild condition is conceptually useful, but could we distinguish an actually blank/missing MEMORY.md from a read failure before resetting the watermark?

MemoryConsolidator currently receives a String, while WorkspaceManager.readWithOverride/readFileQuietly returns "" both when the file is empty/missing and when the filesystem or local read fails (the latter is logged and then collapsed to empty). In that failure case, currentMemory.isBlank() && watermark != EPOCH would reset the watermark and rebuild from all ledgers, potentially overwriting the curated view based on a transient I/O/overlay failure.

Could the read path expose a small status (missing/blank/read-error), or otherwise make the consolidator skip/fail on read errors while retaining the new full-rebuild behavior for a genuinely blank file? The added tests cover blank-file recovery well; a read-error regression test would make this safety boundary explicit.

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Real bug class: clearing or losing MEMORY.md while the consolidation watermark survived would silently drop every pre-watermark day from the curated view. Rebuilding from the full ledger history is the right recovery, the javadoc explains the why, and the tests are thorough. COMMENT only because the rebuild is unbounded and repeatable.

Findings

  • [Warning] MemoryConsolidator.java:171 — full-history rebuild has no size/count cap on the prompt input.
  • [Info] MemoryConsolidator.java:176 — persisted watermark unchanged, so a failing/blank model response re-triggers the expensive rebuild every run.
  • [Info] test — whitespace-only and never-created MEMORY.md cases not asserted.

Suggestions

Bound the rebuild (newest N ledgers or a char budget with a truncation marker) and record an attempt marker so the expensive path runs once per detected loss rather than once per heartbeat.


Automated review by github-manager-bot

// cleared without resetting the watermark: merging only post-watermark ledgers into
// "(empty)" would silently drop every older day from the curated file. Rebuild from
// the full ledger history instead — the same input assembly as a first run.
if (currentMemory.isBlank() && !watermark.equals(Instant.EPOCH)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rebuild input is unbounded. readDailyEntries(rc, Instant.EPOCH) concatenates every memory/*.md ledger with no count or byte cap, so a long-lived workspace whose MEMORY.md was cleared now sends the whole ledger history in one call. The maxMemoryTokens * 4 budget only bounds the MEMORY.md output side, not this user prompt. On a store-backed session with hundreds of daily files this can exceed the context window or dominate cost. Consider taking the newest N ledgers / a max-char budget (logging what was truncated), or consolidating in rolling chunks.

"MEMORY.md is blank while a consolidation watermark exists ({}) — rebuilding"
+ " from all daily ledgers",
watermark);
watermark = Instant.EPOCH;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting the local watermark is right for this run, but the persisted watermark is untouched, so a blank MEMORY.md re-triggers the full rebuild on every heartbeat until the model returns non-blank output (the consolidated.isBlank() path below also returns without advancing it). A cheap lastRebuildAttempt marker, or rebuilding at most once per watermark value, would stop the repeat cost.

Files.exists(localState),
"state file should be written to local disk when no filesystem is configured");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage of rebuild / no-fresh-ledgers / incremental / skip. Two gaps worth pinning: a whitespace-only MEMORY.md (same branch via isBlank()), and the never-created MEMORY.md case — WorkspaceManager.readWithOverride returns "" for both missing and empty, so a stray watermark with no file also rebuilds. Presumably intended; asserting it documents the behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MemoryConsolidator 在 MEMORY.md 缺失或为空时,已有水位导致旧账本不参与重建

4 participants