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
+25 -11
View File
@@ -568,10 +568,10 @@ export default function qmemExtension(pi: ExtensionAPI) {
content: [
{
type: "text",
text: `Correzione applicata: nuovo record ${data.memory_id} supersede ${memoryId}${p.reason ? ` (motivo: ${p.reason})` : ""}. Il vecchio record resta in archivio marcato superseded.`,
text: `Correzione applicata: nuovo record ${data.memory_id} supersede ${memoryId}${p.reason ? ` (motivo: ${p.reason})` : ""}. Il vecchio record resta in archivio marcato superseded.${data.reparented ? ` ${data.reparented} figli ri-parentati al nuovo UUID.` : ""}`,
},
],
details: { new_id: data.memory_id, superseded_id: memoryId },
details: { new_id: data.memory_id, superseded_id: memoryId, reparented: data.reparented ?? 0 },
};
},
});
@@ -774,15 +774,29 @@ export default function qmemExtension(pi: ExtensionAPI) {
}
}
// Recupera tutti i figli associati a parent_id == rootId
const childRes = await gatewayRequest(
cfg,
"POST",
"/v1/memories:search",
{ query: "*", parent_id: rootId, top_k: 20, include_superseded: false, min_score: 0.0 },
signal,
);
const children = childRes.ok ? (childRes.data.results ?? []) : [];
// Recupera tutti i figli associati a parent_id == rootId.
// Fallback lineage: se il root è stato superseduto, i figli storici
// puntano ancora al vecchio UUID (supersedes_id) — li includiamo per
// garantire la navigabilità dell'albero anche per gli orfani pre-fix.
const parentIds = [rootId];
if (rootData.supersedes_id) parentIds.push(rootData.supersedes_id);
const children: any[] = [];
const seen = new Set<string>();
for (const pid of parentIds) {
const childRes = await gatewayRequest(
cfg,
"POST",
"/v1/memories:search",
{ query: "*", parent_id: pid, top_k: 20, include_superseded: false, min_score: 0.0 },
signal,
);
for (const c of childRes.ok ? (childRes.data.results ?? []) : []) {
if (!seen.has(c.memory_id)) {
seen.add(c.memory_id);
children.push(c);
}
}
}
const out: string[] = [];
out.push(`🌳 ALBERO GERARCHICO: ${rootData.topic ?? "ROOT"} [${rootData.level ?? "L1_ROOT"}]`);