feat(reparenting): supersede di un record ri-parenta automaticamente i figli attivi

- gateway: nel blocco supersede, i figli attivi (parent_id == vecchio UUID,
  superseded_by vuoto) vengono ri-parentati al nuovo UUID; audit action
  'reparent'; risposta con campo 'reparented'. Un solo livello: i nipoti
  puntano agli UUID dei figli, invariati. I figli superseduti restano
  storici ancorati alla vecchia lineage.
- estensione: qmem_tree con fallback lineage (cerca figli anche per
  supersedes_id del root) per gli orfani pre-fix; qmem_correct riporta
  il numero di figli ri-parentati.
- test: 2 nuovi (ri-parenta figli attivi; ri-parenta solo attivi con
  figlio già superseduto). 34 pass.
- deploy su brain (10.8.0.3): main.py aggiornato, container ricreato,
  smoke test end-to-end OK (reparented=1).
This commit is contained in:
Matteo Benedetto
2026-08-24 15:43:33 +02:00
parent 0283d3964e
commit e68fca3388
4 changed files with 160 additions and 14 deletions
+32 -1
View File
@@ -593,6 +593,7 @@ async def add_memory(
)
_invalidate_meta()
reparented_count = 0
if superseded_id:
qdrant.set_payload(
collection_name=COLLECTION,
@@ -604,6 +605,31 @@ async def add_memory(
points=[superseded_id],
)
_audit(key, "supersede", old_id=superseded_id, new_id=memory_id, kind=body.kind, agent_id=payload["agent_id"])
# REPARENTING: i figli attivi del record superseduto seguono il nuovo UUID.
# Solo i figli attivi (superseded_by vuoto): le versioni storiche restano
# ancorate alla vecchia lineage; la loro versione attiva ha già ereditato
# il parent_id (qmem_correct) e viene ri-parentata qui. Un solo livello:
# i nipoti puntano agli UUID dei figli, che non cambiano.
children, _ = qdrant.scroll(
collection_name=COLLECTION,
scroll_filter=qm.Filter(
must=[
qm.FieldCondition(key="parent_id", match=qm.MatchValue(value=superseded_id)),
qm.IsEmptyCondition(is_empty=qm.PayloadField(key="superseded_by")),
]
),
limit=1000,
with_payload=False,
)
if children:
qdrant.set_payload(
collection_name=COLLECTION,
payload={"parent_id": memory_id},
points=[p.id for p in children],
)
reparented_count = len(children)
_audit(key, "reparent", old_id=superseded_id, new_id=memory_id, count=reparented_count)
else:
_audit(
key,
@@ -613,7 +639,12 @@ async def add_memory(
agent_id=payload["agent_id"],
guardrail=payload.get("guardrail", {}).get("decision", "ALLOW"),
)
response = {"memory_id": memory_id, "created_at": payload["created_at"], "supersedes_id": superseded_id}
response = {
"memory_id": memory_id,
"created_at": payload["created_at"],
"supersedes_id": superseded_id,
"reparented": reparented_count,
}
if idem_key:
_idempotency[idem_key] = {"hash": _payload_hash(body), "response": response, "ts": time.time()}
return response