fix(memory): progress past completed no-op batches

This commit is contained in:
shixi-li
2026-07-27 02:00:41 +08:00
committed by Xubin Ren
parent b55b76d755
commit 15e42059bd
4 changed files with 98 additions and 33 deletions
+4 -5
View File
@@ -47,9 +47,9 @@ class MemoryStore:
"""Pure file I/O for memory files: MEMORY.md, history.jsonl, SOUL.md, USER.md."""
_DEFAULT_MAX_HISTORY = 1000
# Durable files whose real working-tree delta grounds Dream commit messages
# and the cursor-advance gate. Deliberately excludes memory/.dream_cursor so
# that advancing the cursor itself is never mistaken for a productive edit.
# Durable files whose real working-tree delta grounds Dream commit messages.
# Deliberately excludes memory/.dream_cursor so progress bookkeeping never
# appears as a durable-memory edit in the audit record.
_DREAM_CONTENT_PATHS = ("SOUL.md", "USER.md", "memory/MEMORY.md")
# Per-file cap when embedding current contents into the Dream prompt. The
# durable files are tiny in practice (~5 KB total), but a runaway file must
@@ -586,8 +586,7 @@ class MemoryStore:
"""Structured summary of uncommitted changes to the durable memory files.
Returns "" when git is unavailable or no content file changed. This is
the ground-truth input for diff-grounded Dream commit messages and for
gating cursor advance on real edits (never on LLM self-report).
the ground-truth input for diff-grounded Dream commit messages.
"""
if not self._git.is_initialized():
return ""
+12 -9
View File
@@ -1846,19 +1846,22 @@ def _run_gateway(
tools=store.build_dream_tools(),
on_progress=_silent,
)
# Ground truth: the real file delta, not the LLM's self-report.
# The real file delta grounds the audit record; clean completion
# decides whether this history batch has finished processing.
diff_body = store.dream_content_diff()
productive = bool(diff_body) or (
not store.git.is_initialized()
and MemoryStore.dream_run_completed(resp)
)
if productive:
completed = MemoryStore.dream_run_completed(resp)
if completed:
store.set_last_dream_cursor(last_cursor)
logger.info("Dream cron job completed, cursor advanced to {}", last_cursor)
elif MemoryStore.dream_run_completed(resp):
if diff_body:
logger.info(
"Dream cron job completed, cursor advanced to {}",
last_cursor,
)
else:
logger.info(
"Dream cron job completed with no memory changes; "
"cursor not advanced",
"cursor advanced to {}",
last_cursor,
)
else:
logger.warning(
+6 -7
View File
@@ -437,16 +437,15 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage:
on_progress=_silent,
)
elapsed = time.monotonic() - t0
# Ground truth: the real file delta, not the LLM's self-report.
# The real file delta grounds the audit record; clean completion
# decides whether this history batch has finished processing.
diff_body = store.dream_content_diff()
productive = bool(diff_body) or (
not store.git.is_initialized()
and MemoryStore.dream_run_completed(resp)
)
if productive:
completed = MemoryStore.dream_run_completed(resp)
if completed:
store.set_last_dream_cursor(last_cursor)
if diff_body:
content = f"Dream completed in {elapsed:.1f}s."
elif MemoryStore.dream_run_completed(resp):
else:
content = f"Dream completed in {elapsed:.1f}s; no memory changes."
else:
content = (
+71 -7
View File
@@ -225,7 +225,7 @@ def _build_runnable_dream(
@pytest.mark.asyncio
async def test_dream_advances_cursor_when_diff_nonempty(tmp_path) -> None:
"""A real file delta => productive run => cursor advances (Tier 3)."""
"""A completed run with a real file delta advances the cursor."""
ctx, store = _build_runnable_dream(tmp_path, initialized=True, content_diff="SOUL.md: +1 -0")
await cmd_dream(ctx)
await asyncio.sleep(0)
@@ -233,19 +233,83 @@ async def test_dream_advances_cursor_when_diff_nonempty(tmp_path) -> None:
@pytest.mark.asyncio
async def test_dream_keeps_cursor_on_completed_noop(tmp_path) -> None:
"""Completed run with no file changes must NOT advance the cursor, so the
history batch is reconsidered next run instead of silently swallowed."""
async def test_dream_advances_cursor_on_completed_noop(tmp_path) -> None:
"""A completed no-op has processed the batch and must not repeat it."""
ctx, store = _build_runnable_dream(tmp_path, initialized=True, content_diff="")
await cmd_dream(ctx)
await asyncio.sleep(0)
assert store._last_dream_cursor == 5 # unchanged
assert store._last_dream_cursor == 42
assert "no memory changes" in ctx.loop.bus.outbound[0].content
@pytest.mark.asyncio
async def test_dream_keeps_cursor_when_incomplete_with_diff(tmp_path) -> None:
"""An incomplete run remains retryable even if it left a partial edit."""
ctx, store = _build_runnable_dream(
tmp_path,
initialized=True,
content_diff="SOUL.md: +1 -0",
stop_reason="length",
)
await cmd_dream(ctx)
await asyncio.sleep(0)
assert store._last_dream_cursor == 5
assert "did not complete" in ctx.loop.bus.outbound[0].content
@pytest.mark.asyncio
async def test_dream_noop_batch_unlocks_following_history(tmp_path) -> None:
"""A no-op first batch must not starve later history entries."""
workspace = tmp_path / "workspace"
workspace.mkdir()
store = MemoryStore(workspace)
store.write_soul("# Soul")
store.write_memory("# Memory")
for index in range(1, 22):
store.append_history(f"entry-{index:02d}")
store.git.init()
processed_prompts: list[str] = []
async def process_direct(prompt, *args, **kwargs):
processed_prompts.append(prompt)
return OutboundMessage(
channel="cli",
chat_id="direct",
content="done",
metadata={"_stop_reason": "completed"},
)
msg = InboundMessage(channel="cli", sender_id="u1", chat_id="direct", content="/dream")
bus = _FakeBus()
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
loop = SimpleNamespace(
bus=bus,
context=SimpleNamespace(memory=store, timezone="UTC"),
sessions=SimpleNamespace(sessions_dir=sessions_dir),
process_direct=process_direct,
)
ctx = CommandContext(msg=msg, session=None, key=msg.session_key, raw="/dream", args="", loop=loop)
await cmd_dream(ctx)
await asyncio.sleep(0)
assert len(processed_prompts) == 1
assert "entry-20" in processed_prompts[0]
assert "entry-21" not in processed_prompts[0]
assert store.get_last_dream_cursor() == 20
next_result = store.build_dream_prompt()
assert next_result is not None
next_prompt, next_cursor = next_result
assert next_cursor == 21
assert "entry-21" in next_prompt
assert "entry-01" not in next_prompt
@pytest.mark.asyncio
async def test_dream_non_git_falls_back_to_completion_gate(tmp_path) -> None:
"""Without git there is no diff signal; productivity falls back to the
completion check so non-git workspaces keep working."""
"""Non-git workspaces use the same clean-completion gate."""
ctx, store = _build_runnable_dream(
tmp_path, initialized=False, content_diff="", stop_reason="completed",
)