fix(memory): remove idle archive request fallback

This commit is contained in:
chengyongru
2026-08-19 18:40:20 +08:00
committed by chengyongru
parent 82e50e2c91
commit 960425b3aa
2 changed files with 64 additions and 39 deletions
+19 -16
View File
@@ -1064,7 +1064,11 @@ class Consolidator:
logger.warning("Consolidation provider returned tool calls, raw-dumping to history") logger.warning("Consolidation provider returned tool calls, raw-dumping to history")
self.store.raw_archive(fallback_messages, session_key=session_key) self.store.raw_archive(fallback_messages, session_key=session_key)
return None return None
summary = response.content or "[no summary]" summary = response.content
if not summary or not summary.strip():
logger.warning("Consolidation provider returned no summary, raw-dumping to history")
self.store.raw_archive(fallback_messages, session_key=session_key)
return None
self.store.append_history( self.store.append_history(
summary, summary,
max_chars=_ARCHIVE_SUMMARY_MAX_CHARS, max_chars=_ARCHIVE_SUMMARY_MAX_CHARS,
@@ -1154,19 +1158,21 @@ class Consolidator:
runtime=runtime, runtime=runtime,
) )
if request_messages is None: if request_messages is None:
return await self.archive( logger.debug(
messages, "Idle consolidation cannot replay the full tail for {}; raw-dumping",
runtime=runtime, session.key,
session_key=session.key,
) )
tools = self._get_tool_definitions() self.store.raw_archive(messages, session_key=session.key)
return None
budget = self._input_token_budget(runtime) budget = self._input_token_budget(runtime)
if budget <= 0: if budget <= 0:
return await self.archive( logger.debug(
messages, "Idle consolidation has no safe input budget for {}; raw-dumping",
runtime=runtime, session.key,
session_key=session.key,
) )
self.store.raw_archive(messages, session_key=session.key)
return None
tools = self._get_tool_definitions()
estimated, source = estimate_prompt_tokens_chain( estimated, source = estimate_prompt_tokens_chain(
runtime.provider, runtime.provider,
runtime.model, runtime.model,
@@ -1175,17 +1181,14 @@ class Consolidator:
) )
if estimated > budget: if estimated > budget:
logger.debug( logger.debug(
"Idle consolidation falling back to bounded archive for {}: {}/{} via {}", "Idle consolidation prefix exceeds budget for {}; raw-dumping: {}/{} via {}",
session.key, session.key,
estimated, estimated,
budget, budget,
source, source,
) )
return await self.archive( self.store.raw_archive(messages, session_key=session.key)
messages, return None
runtime=runtime,
session_key=session.key,
)
return await self._archive_request( return await self._archive_request(
request_messages=request_messages, request_messages=request_messages,
fallback_messages=messages, fallback_messages=messages,
+44 -22
View File
@@ -634,6 +634,15 @@ class TestConsolidatorTokenBudget:
class TestCompactIdleSession: class TestCompactIdleSession:
"""Idle compaction tests.""" """Idle compaction tests."""
@pytest.fixture
def runtime(self, mock_provider):
"""Exercise the structured idle-consolidation path by default."""
return LLMRuntime.capture(
mock_provider,
"test-model",
context_window_tokens=128_000,
)
@pytest.fixture @pytest.fixture
def real_consolidator(self, store, mock_provider): def real_consolidator(self, store, mock_provider):
"""Create a Consolidator with a real SessionManager (not a mock).""" """Create a Consolidator with a real SessionManager (not a mock)."""
@@ -713,7 +722,6 @@ class TestCompactIdleSession:
async def test_new_messages_advance_existing_archive_progress( async def test_new_messages_advance_existing_archive_progress(
self, real_consolidator, mock_provider, runtime self, real_consolidator, mock_provider, runtime
): ):
runtime = replace(runtime, context_window_tokens=128_000)
mock_provider.chat_with_retry.return_value = MagicMock( mock_provider.chat_with_retry.return_value = MagicMock(
content="Summary.", finish_reason="stop" content="Summary.", finish_reason="stop"
) )
@@ -773,7 +781,6 @@ class TestCompactIdleSession:
the recent suffix it retains. Otherwise a late user correction / final the recent suffix it retains. Otherwise a late user correction / final
result that lands in the kept suffix is excluded from the persisted result that lands in the kept suffix is excluded from the persisted
summary, leaving a stale wrong conclusion in history. Regression for #4264.""" summary, leaving a stale wrong conclusion in history. Regression for #4264."""
runtime = replace(runtime, context_window_tokens=128_000)
mock_provider.chat_with_retry.return_value = MagicMock( mock_provider.chat_with_retry.return_value = MagicMock(
content="Summary.", finish_reason="stop" content="Summary.", finish_reason="stop"
) )
@@ -934,7 +941,6 @@ class TestCompactIdleSession:
self, real_consolidator, mock_provider, runtime self, real_consolidator, mock_provider, runtime
): ):
"""30 turns with last_consolidated=50 → only unconsolidated tail considered.""" """30 turns with last_consolidated=50 → only unconsolidated tail considered."""
runtime = replace(runtime, context_window_tokens=128_000)
mock_provider.chat_with_retry.return_value = MagicMock( mock_provider.chat_with_retry.return_value = MagicMock(
content="Tail summary.", finish_reason="stop" content="Tail summary.", finish_reason="stop"
) )
@@ -972,7 +978,6 @@ class TestCompactIdleSession:
mock_provider, mock_provider,
runtime, runtime,
): ):
runtime = replace(runtime, context_window_tokens=128_000)
mock_provider.chat_with_retry.return_value = MagicMock( mock_provider.chat_with_retry.return_value = MagicMock(
content="Tail summary.", finish_reason="stop" content="Tail summary.", finish_reason="stop"
) )
@@ -1023,7 +1028,6 @@ class TestCompactIdleSession:
store, store,
runtime, runtime,
): ):
runtime = replace(runtime, context_window_tokens=128_000)
tools = [{"type": "function", "function": {"name": "lookup"}}] tools = [{"type": "function", "function": {"name": "lookup"}}]
real_consolidator._get_tool_definitions.return_value = tools real_consolidator._get_tool_definitions.return_value = tools
mock_provider.chat_with_retry.return_value = LLMResponse( mock_provider.chat_with_retry.return_value = LLMResponse(
@@ -1102,23 +1106,44 @@ class TestCompactIdleSession:
assert sessions.get_or_create("cli:unexpected-tool").last_consolidated == 2 assert sessions.get_or_create("cli:unexpected-tool").last_consolidated == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_oversized_prefix_falls_back_to_bounded_archive( async def test_empty_response_uses_raw_fallback(
self, self,
real_consolidator, real_consolidator,
mock_provider, mock_provider,
store, store,
runtime, runtime,
): ):
async def bounded_chat(**kwargs): mock_provider.chat_with_retry.return_value = LLMResponse(
sent_chars = sum( content="",
len(str(message.get("content") or "")) finish_reason="stop",
for message in kwargs["messages"]
) )
if sent_chars > 20_000: sessions = real_consolidator.sessions
return LLMResponse(content="context too long", finish_reason="error") session = sessions.get_or_create("cli:empty-summary")
return LLMResponse(content="Bounded summary.", finish_reason="stop") session.add_message("user", "remember this")
session.add_message("assistant", "important answer")
sessions.save(session)
mock_provider.chat_with_retry.side_effect = bounded_chat result = await real_consolidator.compact_idle_session(
"cli:empty-summary",
runtime=runtime,
)
assert result is None
entries = store.read_unprocessed_history(since_cursor=0)
assert len(entries) == 1
assert entries[0]["content"].startswith("[RAW] ")
assert "important answer" in entries[0]["content"]
assert sessions.get_or_create("cli:empty-summary").last_consolidated == 2
@pytest.mark.asyncio
async def test_oversized_prefix_raw_archives_without_flattened_llm_retry(
self,
real_consolidator,
mock_provider,
store,
runtime,
):
runtime = replace(runtime, context_window_tokens=1_000)
sessions = real_consolidator.sessions sessions = real_consolidator.sessions
session = sessions.get_or_create("sdk:oversized") session = sessions.get_or_create("sdk:oversized")
session.add_message("user", "x" * 100_000) session.add_message("user", "x" * 100_000)
@@ -1129,13 +1154,11 @@ class TestCompactIdleSession:
runtime=runtime, runtime=runtime,
) )
assert result == "Bounded summary." assert result is None
sent = mock_provider.chat_with_retry.call_args.kwargs["messages"] mock_provider.chat_with_retry.assert_not_awaited()
assert sum(len(str(message.get("content") or "")) for message in sent) < 20_000 entries = store.read_unprocessed_history(since_cursor=0)
assert not any( assert len(entries) == 1
"[RAW]" in entry["content"] assert entries[0]["content"].startswith("[RAW] ")
for entry in store.read_unprocessed_history(since_cursor=0)
)
assert sessions.get_or_create("sdk:oversized").last_consolidated == 1 assert sessions.get_or_create("sdk:oversized").last_consolidated == 1
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1145,7 +1168,6 @@ class TestCompactIdleSession:
mock_provider, mock_provider,
runtime, runtime,
): ):
runtime = replace(runtime, context_window_tokens=128_000)
mock_provider.chat_with_retry.return_value = LLMResponse( mock_provider.chat_with_retry.return_value = LLMResponse(
content="Summary.", content="Summary.",
finish_reason="stop", finish_reason="stop",