feat(sync): tombstone, pull dall'export, ritocchi flush e fallback su 404
Prepara il client al gateway 2.12.0 (export + soft delete), mantenendo la compatibilità con la 2.11.0 in produzione fino al redeploy. - tombstone: colonna `deleted_at` (+ migrazione), monotona nel merge, esclusa dalle ricerche locali di default; `--deleted` in CLI e /qmem:local find; conteggio nel report/status; marker 🗑 nei risultati di qmem_get - `pull` usa `include_deleted=true` e mappa l'intero payload dell'export (incluso deleted_at), così il mirror impara le cancellazioni - rate limit: pace del flush 600 ms (100 req/min < 120/min del gateway) e messaggio dedicato su 429 (prima 300-400 ms → possibile 429 con code grandi) - `qmem_get`: fallback sull'indice locale anche sul 404 (un id in coda non è ancora sul gateway) con etichetta "non presente sul gateway" - test: 27 controlli (nuovi: pull con tombstone, esclusione/visibilità tombstone, ricerca del record esportato) Verificato con la suite locale completa: 27/27.
This commit is contained in:
@@ -64,6 +64,7 @@ if (cmd === "status") {
|
||||
if (r.queueLastError) console.log(` ultimo errore : ${r.queueLastError.slice(0, 120)}`);
|
||||
}
|
||||
if (r.pendingInIndex) console.log(` in indice : ${r.pendingInIndex} record marcati ⏳ (creati offline, non ancora sul gateway)`);
|
||||
if (r.deleted) console.log(` tombstone : ${r.deleted} record cancellati sul gateway (soft delete)`);
|
||||
console.log(` top progetti : ${r.topProjects.map((p) => `${p.project_id ?? "(null)"}=${p.n}`).join(", ") || "-"}`);
|
||||
if (r.duplicates.length) {
|
||||
console.log(` possibili duplicati (testo identico): ${r.duplicates.length} gruppi — es. ${r.duplicates[0].ids.map((i) => i.slice(0, 8)).join(", ")}`);
|
||||
@@ -86,7 +87,7 @@ if (cmd === "status") {
|
||||
} else if (cmd === "find") {
|
||||
const query = argv[1] && !argv[1].startsWith("--") ? argv[1] : "";
|
||||
if (!query) {
|
||||
console.error('Uso: find "query" [--kind K] [--project P] [--top N] [--all] [--exact] [--json]');
|
||||
console.error('Uso: find "query" [--kind K] [--project P] [--top N] [--all] [--deleted] [--exact] [--json]');
|
||||
process.exit(2);
|
||||
}
|
||||
const hits = await localSearch(
|
||||
@@ -99,6 +100,7 @@ if (cmd === "status") {
|
||||
topic: typeof opt("topic", null) === "string" ? opt("topic", null) : undefined,
|
||||
include_superseded: has("all"),
|
||||
include_private: has("private"),
|
||||
include_deleted: has("deleted"),
|
||||
exact: has("exact"),
|
||||
top_k: Number(opt("top", 5)) || 5,
|
||||
},
|
||||
|
||||
+19
-2
@@ -33,6 +33,8 @@ const ID_B = "22222222-2222-4222-8222-222222222222"; // store poi superseduto
|
||||
const ID_C = "33333333-3333-4333-8333-333333333333"; // correzione (nuovo)
|
||||
const ID_D = "44444444-4444-4444-8444-444444444444"; // visto in qmem_search (senza project_id)
|
||||
const ID_E = "55555555-5555-4555-8555-555555555555"; // visto in qmem_get
|
||||
const EXPORT_ID = "66666666-6666-4666-8666-666666666666"; // dal pull export
|
||||
const TOMBSTONE_ID = "77777777-7777-4777-8777-777777777777"; // tombstone dal pull export
|
||||
|
||||
fs.mkdirSync(SESS_DIR, { recursive: true });
|
||||
fs.mkdirSync(path.dirname(CONFIG), { recursive: true });
|
||||
@@ -181,7 +183,16 @@ const server = createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
if (req.url?.startsWith("/v1/memories:export")) {
|
||||
res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ detail: "non trovato" }));
|
||||
res.writeHead(200, { "content-type": "application/json" }).end(
|
||||
JSON.stringify({
|
||||
results: [
|
||||
{ memory_id: EXPORT_ID, text: "Record arrivato dal pull export del gateway.", kind: "fact", project_id: "export-proj", scope: "agent", created_at: "2026-09-10T10:00:00Z", updated_at: "2026-09-10T10:00:00Z", updated_ts: 1 },
|
||||
{ memory_id: TOMBSTONE_ID, text: "Record cancellato sul gateway con soft delete.", kind: "fact", project_id: "export-proj", created_at: "2026-09-09T10:00:00Z", deleted_at: "2026-09-11T10:00:00Z", deleted_ts: 2 },
|
||||
],
|
||||
count: 2,
|
||||
next_cursor: null,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (req.url === `/v1/memories/${ID_D}`) {
|
||||
@@ -210,7 +221,13 @@ check("project_id/private arricchiti nel DB", afterJson.withProject > 0, `con pr
|
||||
const pullOut = await runCliAsync(["pull", "--json"]);
|
||||
const pull = { stdout: pullOut };
|
||||
const pullJson = JSON.parse(pull.stdout || "{}");
|
||||
check("pull segnala endpoint export assente", pullJson.supported === false, pullJson.message ?? "");
|
||||
check("pull export: 2 record (1 attivo + 1 tombstone)", pullJson.supported === true && pullJson.fetched === 2, `pagine=${pullJson.pages} record=${pullJson.fetched}`);
|
||||
const findExported = JSON.parse(runCli(["find", "pull export gateway", "--json"]).stdout || "[]");
|
||||
check("record esportato ricercabile nel mirror", findExported.some((h) => h.memory_id === EXPORT_ID), `${findExported.length} hit`);
|
||||
const findTombstone = JSON.parse(runCli(["find", "soft delete", "--json"]).stdout);
|
||||
check("tombstone escluso dalle ricerche locali di default", !findTombstone.some((h) => h.memory_id === TOMBSTONE_ID), `${findTombstone.length} hit`);
|
||||
const findTombstoneAll = JSON.parse(runCli(["find", "soft delete", "--deleted", "--json"]).stdout || "{}");
|
||||
check("tombstone visibile con --deleted", findTombstoneAll.some((h) => h.memory_id === TOMBSTONE_ID && h.deleted_at), `${findTombstoneAll.length} hit`);
|
||||
|
||||
// ---------------------------------------------------------------- 5) outbox: flush, 409, 422, supersede
|
||||
const q1 = JSON.parse((await runCliAsync(["queue", "--json"])) || "{}");
|
||||
|
||||
Reference in New Issue
Block a user