fix(LongTermMemory): reject summaries that cannot fit recall context

This commit is contained in:
huanghuoguoguo
2026-11-02 21:13:10 +08:00
parent dd95ff9796
commit 5627a3b9bc
3 changed files with 24 additions and 0 deletions
@@ -650,3 +650,6 @@ Active sources remain active; this feature does not promise lossy compression or
Existing historical summary records without lineage cannot be retroactively traced. They are not migrated automatically. Semantic drafting and explicit approval are also available through the Memory Console review panel, so processor-only deployments do not need a Pipeline command route.
Document deletion invalidates dependent summaries and pending drafts for every affected memory scope before removing imported records. Deleting a knowledge base clears only that collection's pending drafts. Preview generation and collection deletion share the same collection lock, so an in-flight model preview cannot save source text after deletion completes; a preview for a removed KB is rejected before calling the model. Each model call holding that lock is capped at 30 seconds; queued callers and storage operations still follow their normal API lifecycle.
A reviewed summary must also fit the bounded recall record budget before publication; oversized summaries are rejected without writing an episode or hiding source facts. Reduce the consolidation candidate count and preview again. This is a character/serialization bound, not an exact token estimate.
@@ -10,6 +10,7 @@ from datetime import datetime, timezone
from langbot_plugin.api.entities.builtin.provider.message import Message
from store.episode_provenance import normalize_provenance, read_provenance
from store.recall import select_memories
MODEL_TIMEOUT_SECONDS = 30
PROPOSAL_PREFIX = "ltm.consolidation.proposal."
@@ -204,6 +205,9 @@ async def apply_summary(store, collection_id: str, embedding_model_uuid: str, us
"consolidation_source_ids": json.dumps(proposal["source_ids"]),
"consolidation_source_hashes": json.dumps(proposal["source_hashes"], sort_keys=True),
}
record = {"id": episode_id, "metadata": meta, "document": content, "score": None, "distance": 0.0}
if not select_memories([record], "", top_k=1):
raise ValueError("Summary exceeds the recall budget; reduce consolidation candidates and preview again")
vectors = await store.plugin.invoke_embedding(embedding_model_uuid, [content])
await store._writes.mutate(
store.plugin,
@@ -345,3 +345,20 @@ async def test_summary_verification_allows_host_added_scope_metadata():
assert result["summary_episode"]["status"] == "active"
assert result["summary_episode"]["metadata"]["_langbot_workspace_uuid"] == "workspace-test"
assert plugin.records["source"]["metadata"]["status"] == "active"
@pytest.mark.asyncio
async def test_summary_too_large_for_recall_is_not_published():
plugin = SemanticPlugin()
add_source(plugin, "one", "First source fact")
add_source(plugin, "two", "Second source fact")
plugin.response = {"facts": [
{"text": "a" * 2000, "source_ids": ["one"]},
{"text": "b" * 2000, "source_ids": ["two"]},
]}
store = MemoryStore(plugin)
draft = await preview(store)
with pytest.raises(ValueError, match="recall budget"):
await store.apply_consolidation("kb", "emb", "scope", proposal_id=draft["proposal_id"])
assert set(plugin.records) == {"one", "two"}
assert all(item["metadata"]["status"] == "active" for item in plugin.records.values())