#!/usr/bin/env node /** * One-time migration for kb#153 / A2A-21 (DESIGN-a2a-agents.md v2.1 §5b): * splits the single legacy "adolf" Hindsight bank into the per-human bank * layout the hindsight-memory plugin now expects (see that plugin's * index.js / resolveBanksForSender). * * WHY A STRAIGHT COPY, NOT A alvis-vs-household CLASSIFIER: * The live "adolf" bank's memories/list `context` field (chatLabel, set by * the plugin's pre-kb#153 code) shows exactly ONE Matrix DM room across all * 381 facts (`chat_qxknyifrguyghhvzdb_mtx_alogins_net` / * `chat_room_qxknyifrguyghhvzdb_mtx_alogins_net`) plus a handful of * non-Matrix contexts (`chat_webchat`, blank, and manual dev-seeded labels * like "goals"/"work"/"kb#84 smoke test"). None of it is attributable to * elizaveta (she was only just added to the DM allowlist) and there is no * reliable signal in the data for "this fact is household, not personal" — * that is a content judgment call, and DESIGN §5b's hard rule is that * promotion from a private bank to the shared bank happens ONLY by the * owning human's explicit action/approval task, never automatically. So the * correct, safe migration is: everything goes to adolf-alvis (matching "the * default is H's private bank"); nothing is auto-promoted to adolf-shared. * alvis can promote individual household facts to adolf-shared later, * through whatever explicit approval flow gets built for that (kb#153's * report flags this as follow-up work, not done by this script). * * MECHANISM: Hindsight has no bulk "copy raw fact between banks" endpoint * (verified against the live OpenAPI schema — /export and /import are bank * TEMPLATE manifests: config/mental-models/directives, not memory data). * The only write path is POST .../memories (RetainRequest), which re-runs * server-side extraction on each item's `content` text. Since source items * are already atomic single facts (Hindsight's own extraction output), this * script feeds each fact's already-clean `text` back through retain into * the destination bank, carrying over `context` and `timestamp` (`date`) * for provenance. Re-extraction on an already-atomic fact is expected to * reproduce it closely, not fragment it further, but this is a genuine * re-processing step (a live LLM call per item via hindsight-llm), not a * byte-for-byte copy — verify counts after running. * * SAFETY: dry-run by default. Requires --execute to write. Refuses to * target the source bank as its own destination. Does NOT delete or modify * the source bank — this script only ever reads it. * * Usage: * node migrate-adolf-memory-banks.mjs --source adolf --dest adolf-alvis [--execute] * node migrate-adolf-memory-banks.mjs --source adolf --dest adolf-alvis --async --execute * * Tested (kb#153) against a throwaway destination bank with the full live * "adolf" source in dry-run + a partial real write, then that throwaway * bank was deleted — this script has NOT been run against adolf-alvis. That * final execution against the real destination is the live-migration step * kb#153 explicitly hands off rather than running unattended. */ const args = process.argv.slice(2); function argVal(name, def) { const i = args.indexOf(`--${name}`); return i !== -1 && args[i + 1] !== undefined ? args[i + 1] : def; } const flag = (name) => args.includes(`--${name}`); const HINDSIGHT_URL = argVal("hindsight-url", "http://localhost:8888").replace(/\/+$/, ""); const SOURCE = argVal("source", "adolf"); const DEST = argVal("dest", "adolf-alvis"); const EXECUTE = flag("execute"); const ASYNC = flag("async"); const PAGE_SIZE = Number(argVal("page-size", "50")); const DELAY_MS = Number(argVal("delay-ms", ASYNC ? "150" : "1500")); // Testing/smoke-test aid only — omit to migrate everything. const LIMIT = argVal("limit", undefined); if (SOURCE === DEST) { console.error(`Refusing: --source and --dest are both "${SOURCE}".`); process.exit(1); } function bankPath(bank) { return `${HINDSIGHT_URL}/v1/default/banks/${encodeURIComponent(bank)}`; } async function listAll(bank) { const items = []; let offset = 0; for (;;) { const res = await fetch(`${bankPath(bank)}/memories/list?limit=${PAGE_SIZE}&offset=${offset}`); if (!res.ok) throw new Error(`list ${bank} failed: ${res.status}`); const data = await res.json(); const batch = Array.isArray(data.items) ? data.items : []; items.push(...batch); offset += batch.length; if (batch.length === 0 || offset >= (data.total ?? offset)) break; } return items; } async function retainOne(bank, item) { const memoryItem = { content: item.text, context: item.context || "migrated_from_adolf", timestamp: item.date || item.mentioned_at || undefined, }; const res = await fetch(`${bankPath(bank)}/memories`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ async: ASYNC, items: [memoryItem] }), }); if (!res.ok) { const body = await res.text().catch(() => ""); throw new Error(`retain into ${bank} failed: ${res.status} ${body.slice(0, 200)}`); } return res.json(); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } async function main() { console.log(`Source: ${SOURCE} Dest: ${DEST} Mode: ${EXECUTE ? "EXECUTE" : "DRY-RUN"} async retain: ${ASYNC}`); let items = await listAll(SOURCE); console.log(`Fetched ${items.length} memory items from "${SOURCE}".`); if (LIMIT) { items = items.slice(0, Number(LIMIT)); console.log(`--limit set: only processing first ${items.length} items (testing aid).`); } if (items.length === 0) { console.log("Nothing to migrate."); return; } console.log("Sample of first 3 items to be migrated:"); for (const it of items.slice(0, 3)) { console.log(` [${it.fact_type}] ${it.text.slice(0, 100)}${it.text.length > 100 ? "…" : ""} (context=${it.context || "-"})`); } if (!EXECUTE) { console.log(`\nDry-run only — no writes made. Re-run with --execute to retain all ${items.length} items into "${DEST}".`); return; } let ok = 0; let failed = 0; for (const [i, item] of items.entries()) { try { await retainOne(DEST, item); ok++; } catch (e) { failed++; console.error(` [${i + 1}/${items.length}] FAILED: ${e.message}`); } if ((i + 1) % 10 === 0 || i === items.length - 1) { console.log(` ${i + 1}/${items.length} processed (ok=${ok}, failed=${failed})`); } await sleep(DELAY_MS); } console.log(`\nDone. ok=${ok} failed=${failed} out of ${items.length}.`); console.log(`Verify with: GET ${bankPath(DEST)}/stats`); } main().catch((e) => { console.error(e); process.exit(1); });