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
163 lines
6.6 KiB
JavaScript
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);
|
|
});
|