Files
AgapHost/ai/migrate-adolf-memory-banks.mjs
alvis 9094d71e2f ai: migrate LLM backbone from Kimi CLI to Codex CLI
Retires the Moonshot/Kimi subscription in favour of the already-paid ChatGPT
plan. Both CLI wrappers now run `codex exec`; the kimi-agent container is gone.

adolf-llm + hindsight-llm:
- runKimi -> runCodex (`codex exec --json --skip-git-repo-check`), resume via
  `codex exec resume <thread_id>`.
- MCP moves from a per-session .mcp.json (a workaround for Kimi having no
  --mcp-config-file flag) to a $CODEX_HOME/config.toml generated once at
  startup from shared-mcp.json. Field translation is load-bearing:
  bearerTokenEnvVar -> bearer_token_env_var, enabledTools -> enabled_tools.
- approval_policy="never" + sandbox_mode required, or unattended turns block
  on an approval prompt nobody can answer.

kimi-agent removed. It was the ONLY large-tier deployment behind LiteLLM, so
deleting it outright would have silently degraded every large-tier request to
the local 4B model via the existing fallbacks. tier-large, the auto_router
complex-reasoning route and their fallbacks now point at the codex-backed
adolf-llm wrapper (model_name: codex-agent).

Three environment blockers fixed along the way:
- OpenAI geo-blocks this host (403 unsupported_country_region_territory).
  Both containers now egress via the host xray proxy, with NO_PROXY keeping
  MCP and *.alogins.net traffic off the tunnel.
- node:22-slim ships no system CA store; the Rust codex binary validates TLS
  against it, so every HTTPS call failed with a generic transport error while
  Node's own fetch worked. ca-certificates added to both images.
- `codex exec resume` rejects -C/--cd (plain `codex exec` accepts it), which
  broke follow-up turns while first turns succeeded.

Known regression: Kimi's managed-usage API has no Codex equivalent, so the
/usage route returns 501 and there is no quota probe for the codex model.
The two quota plugins degrade quietly to no output.

Also: stop tracking cognee.env (live LLM + JWT secrets) and gitignore it.
The secrets remain in earlier history and should be rotated.

Verified live: plain turn, SSE streaming, session resume, MCP tool call,
bearer-token MCP call, and completions through both LiteLLM routes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Y5QPagv4iun1ghpwM96Ff
2026-08-01 06:13:27 +00:00

163 lines
6.6 KiB
JavaScript

#!/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);
});