feat(agent): two-stage memory system with Dream consolidation

Replace single-stage MemoryConsolidator with a two-stage architecture:

- Consolidator: lightweight token-budget triggered summarization,
  appends to HISTORY.md with cursor-based tracking
- Dream: cron-scheduled two-phase processor that analyzes HISTORY.md
  and updates SOUL.md, USER.md, MEMORY.md via AgentRunner with
  edit_file tools for surgical, fault-tolerant updates

New files: MemoryStore (pure file I/O), Dream class, DreamConfig,
/dream and /dream-log commands. 89 tests covering all components.
This commit is contained in:
chengyongru
2026-04-02 22:42:25 +08:00
committed by chengyongru
parent 7113ad34f4
commit b9616674f0
18 changed files with 856 additions and 728 deletions
+40 -2
View File
@@ -47,7 +47,7 @@ async def cmd_status(ctx: CommandContext) -> OutboundMessage:
session = ctx.session or loop.sessions.get_or_create(ctx.key)
ctx_est = 0
try:
ctx_est, _ = loop.memory_consolidator.estimate_session_prompt_tokens(session)
ctx_est, _ = loop.consolidator.estimate_session_prompt_tokens(session)
except Exception:
pass
if ctx_est <= 0:
@@ -75,13 +75,47 @@ async def cmd_new(ctx: CommandContext) -> OutboundMessage:
loop.sessions.save(session)
loop.sessions.invalidate(session.key)
if snapshot:
loop._schedule_background(loop.memory_consolidator.archive_messages(snapshot))
loop._schedule_background(loop.consolidator.archive(snapshot))
return OutboundMessage(
channel=ctx.msg.channel, chat_id=ctx.msg.chat_id,
content="New session started.",
)
async def cmd_dream(ctx: CommandContext) -> OutboundMessage:
"""Manually trigger a Dream consolidation run."""
loop = ctx.loop
try:
did_work = await loop.dream.run()
content = "Dream completed." if did_work else "Dream: nothing to process."
except Exception as e:
content = f"Dream failed: {e}"
return OutboundMessage(
channel=ctx.msg.channel, chat_id=ctx.msg.chat_id, content=content,
)
async def cmd_dream_log(ctx: CommandContext) -> OutboundMessage:
"""Show the Dream consolidation log."""
loop = ctx.loop
store = loop.consolidator.store
log = store.read_dream_log()
if not log:
# Check if Dream has ever processed anything
if store.get_last_dream_cursor() == 0:
content = "Dream has not run yet."
else:
content = "No dream log yet."
else:
content = f"## Dream Log\n\n{log}"
return OutboundMessage(
channel=ctx.msg.channel,
chat_id=ctx.msg.chat_id,
content=content,
metadata={"render_as": "text"},
)
async def cmd_help(ctx: CommandContext) -> OutboundMessage:
"""Return available slash commands."""
return OutboundMessage(
@@ -100,6 +134,8 @@ def build_help_text() -> str:
"/stop — Stop the current task",
"/restart — Restart the bot",
"/status — Show bot status",
"/dream — Manually trigger Dream consolidation",
"/dream-log — Show Dream consolidation log",
"/help — Show available commands",
]
return "\n".join(lines)
@@ -112,4 +148,6 @@ def register_builtin_commands(router: CommandRouter) -> None:
router.priority("/status", cmd_status)
router.exact("/new", cmd_new)
router.exact("/status", cmd_status)
router.exact("/dream", cmd_dream)
router.exact("/dream-log", cmd_dream_log)
router.exact("/help", cmd_help)