feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s

Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation.

Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
This commit is contained in:
Codex
2026-09-10 10:31:34 +02:00
parent 8fe526dd6e
commit 82e2c91f42
168 changed files with 11914 additions and 1772 deletions
+40 -100
View File
@@ -259,95 +259,11 @@ def test_clear_drops_reference_without_deleting_memory_collection():
assert not any(method == "DELETE" and url.endswith("/collections/demo-memory") for method, url in calls)
def test_clear_can_create_memory_collection_before_payload_indexes_become_visible():
def test_clear_does_not_read_or_import_legacy_memory():
calls = []
memory_created = False
def request(method, url, *, json=None, timeout=None):
nonlocal memory_created
calls.append((method, url, json))
if method == "GET" and url.endswith("/collections/demo"):
return FakeResponse(200, {"result": {}})
if method == "GET" and url.endswith("/collections/demo-memory"):
if not memory_created:
return FakeResponse(404, {"status": "error"})
return FakeResponse(200, {
"result": {
"config": {"params": {"vectors": {"size": 1024, "distance": "Cosine"}}},
# Qdrant exposes newly-created payload indexes asynchronously.
"payload_schema": {},
}
})
if method == "PUT" and url.endswith("/collections/demo-memory"):
memory_created = True
return FakeResponse(200, {"status": "ok"})
if method == "PUT" and url.endswith("/collections/demo-memory/index"):
return FakeResponse(200, {"status": "ok"})
if method == "POST" and url.endswith("/collections/demo/points/scroll"):
return FakeResponse(200, {
"result": {"points": [], "next_page_offset": None}
})
if method == "DELETE" and url.endswith("/collections/demo"):
return FakeResponse(200, {"status": "ok"})
if method == "GET" and url.endswith("/collections/demo-reference"):
return FakeResponse(404, {"status": "error"})
raise AssertionError((method, url, json))
store = QdrantVectorStore(
base_url="http://qdrant:6333",
collections={"reference": "demo-reference", "memory": "demo-memory"},
workspace_id="demo",
expected_dimension=1024,
collection_lifecycle="require_existing",
request=request,
)
assert store.clear_reference() is True
assert memory_created is True
assert any(method == "DELETE" and url.endswith("/collections/demo") for method, url, _ in calls)
def test_clear_migrates_legacy_memory_before_retiring_shared_collection():
calls = []
legacy_point = {
"id": "legacy-memory-id",
"vector": [0.25] * 1024,
"payload": {
"workspace_id": "demo",
"kind": "memory",
"record_kind": "memory",
"record_key": "memory:legacy",
"content_hash": "sha256:" + "a" * 64,
},
}
ready_memory = {
"result": {
"config": {"params": {"vectors": {"size": 1024, "distance": "Cosine"}}},
"payload_schema": {
field: {"data_type": "keyword"}
for field in (
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
)
},
}
}
def request(method, url, *, json=None, timeout=None):
calls.append((method, url, json))
if method == "GET" and url.endswith("/collections/demo"):
return FakeResponse(200, {"result": {}})
if method == "GET" and url.endswith("/collections/demo-memory"):
return FakeResponse(200, ready_memory)
if method == "POST" and url.endswith("/collections/demo/points/scroll"):
assert json["with_vector"] is True
return FakeResponse(200, {
"result": {"points": [legacy_point], "next_page_offset": None}
})
if method == "PUT" and "/collections/demo-memory/points?wait=true" in url:
return FakeResponse(200, {"status": "ok"})
if method == "DELETE" and url.endswith("/collections/demo"):
return FakeResponse(200, {"status": "ok"})
calls.append((method, url))
if method == "GET" and url.endswith("/collections/demo-reference"):
return FakeResponse(200, {"result": {}})
if method == "DELETE" and url.endswith("/collections/demo-reference"):
@@ -357,23 +273,13 @@ def test_clear_migrates_legacy_memory_before_retiring_shared_collection():
store = QdrantVectorStore(
base_url="http://qdrant:6333",
collections={"reference": "demo-reference", "memory": "demo-memory"},
workspace_id="demo",
expected_dimension=1024,
request=request,
workspace_id="demo", expected_dimension=1024, request=request,
)
assert store.clear_reference() is True
migrated = next(
payload for method, url, payload in calls
if method == "PUT" and "/collections/demo-memory/points?wait=true" in url
)
assert migrated == {"points": [legacy_point]}
deleted = [url for method, url, _payload in calls if method == "DELETE"]
assert deleted == [
"http://qdrant:6333/collections/demo",
"http://qdrant:6333/collections/demo-reference",
assert calls == [
("GET", "http://qdrant:6333/collections/demo-reference"),
("DELETE", "http://qdrant:6333/collections/demo-reference"),
]
assert not any(url.endswith("/collections/demo-memory") for url in deleted)
def _ready_collection_with_bm25(fake: FakeQdrantHttp) -> None:
@@ -718,6 +624,40 @@ def test_search_filters_by_workspace_and_allowed_record_kinds():
}
@pytest.mark.parametrize("kind,family", [("memory", "domain_clarification"),
("solved_question", "solved_question")])
def test_memory_hybrid_applies_identical_scope_to_both_prefetch_branches(kind, family):
from tht.memory.retrieval import RecallScope
fake = FakeQdrantHttp()
_ready_collection_with_bm25(fake)
store = _store(fake)
scope = RecallScope(database="dwh", schema_name="sales", table="orders", column="id",
scope="Sales", concepts=["grain"])
store.search(["memory"], [0.2] * 1024, limit=5, kinds=[kind],
query_text="Order grain", query_language="english",
metadata_filter=scope.vector_filter(family))
query = next(call[2] for call in reversed(fake.calls) if call[1].endswith("/points/query"))
dense, lexical = query["prefetch"]
assert dense["filter"] == lexical["filter"]
must = dense["filter"]["must"]
assert {"key": "workspace_id", "match": {"value": "demo"}} in must
assert {"key": "memory_family", "match": {"value": family}} in must
assert {"key": "memory_format", "match": {"value": 2}} in must
assert {"key": "memory_scope", "match": {"value": "Sales"}} in must
assert {"key": "memory_concepts", "match": {"value": "grain"}} in must
nested = must[-1]["should"][1]["nested"]
assert nested["key"] == "memory_dependencies"
assert nested["filter"]["must"] == [
{"key": "database", "match": {"value": "dwh"}},
{"key": "schema_name", "match": {"any": ["", "sales"]}},
{"key": "table", "match": {"any": ["", "orders"]}},
{"key": "column", "match": {"any": ["", "id"]}},
]
assert lexical["using"] == "bm25"
assert query["query"] == {"rrf": {}}
def test_evidence_search_refuses_dense_only_fallback():
store = _store(FakeQdrantHttp())