Files
brain/tools/ruflo-h7-patch.mjs

201 lines
7.7 KiB
JavaScript

#!/usr/bin/env node
/**
* ruflo H7 patch re-apply tool.
*
* H7: `ruflo memory store` routes through the AgentDB-v3 bridge
* (memory-bridge.js:bridgeStoreEntry) which inserts into an in-RAM sql.js DB
* and never flushes to disk — entries are lost between CLI invocations.
*
* Fix: force getBridge() in @claude-flow/cli's memory-initializer.js to
* return null, so memory ops fall through to the raw sql.js path that DOES
* persist (db.export() + writeFileRestricted).
*
* Both operations live in the GLOBAL npm install and are overwritten by
* `ruflo update` — re-run this script after any ruflo upgrade.
* Each apply/revert/check now covers BOTH the getBridge patch AND the
* onnxruntime-node dedupe (disabling the nested @xenova/transformers copy).
*
* Usage:
* node tools/ruflo-h7-patch.mjs apply both fixes (idempotent)
* node tools/ruflo-h7-patch.mjs --check exit 0 if both applied, 1 if not
* node tools/ruflo-h7-patch.mjs --revert remove both fixes
*
* See docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md §4
*/
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
export const PATCH_MARKER = 'LIDERRA-H7-PATCH';
export const PATCH_LINE =
' return null; /* LIDERRA-H7-PATCH: AgentDB bridge writes in-RAM sql.js, never flushes — force raw persisting path */';
const ANCHOR = 'async function getBridge() {';
/** True if the file content already contains the patch. */
export function isPatched(content) {
return content.includes(PATCH_MARKER);
}
/**
* Insert the patch line as the first statement of getBridge().
* Idempotent. Throws if the anchor is absent (ruflo upstream changed).
*/
export function applyPatch(content) {
if (isPatched(content)) return { content, changed: false };
const idx = content.indexOf(ANCHOR);
if (idx === -1) {
throw new Error(
`anchor "${ANCHOR}" not found — ruflo upstream changed getBridge(); revise this patch manually`,
);
}
const insertAt = idx + ANCHOR.length;
const patched = content.slice(0, insertAt) + '\n' + PATCH_LINE + content.slice(insertAt);
return { content: patched, changed: true };
}
/** Remove the patch line (the line carrying PATCH_MARKER). */
export function revertPatch(content) {
if (!isPatched(content)) return { content, changed: false };
const lines = content.split('\n');
const kept = lines.filter((l) => !l.includes(PATCH_MARKER));
return { content: kept.join('\n'), changed: true };
}
/**
* Resolve the path to @claude-flow/cli's memory-initializer.js inside the
* global npm tree. Handles both nested (under ruflo/) and hoisted layouts.
*/
export function resolveTargetPath(npmRoot) {
const tail = ['@claude-flow', 'cli', 'dist', 'src', 'memory', 'memory-initializer.js'];
const candidates = [
join(npmRoot, 'ruflo', 'node_modules', ...tail),
join(npmRoot, ...tail),
];
return candidates.find((c) => existsSync(c)) ?? null;
}
// --- onnxruntime-node dedupe (H7 fix, part 2) ---
// ruflo's dependency tree carries two incompatible onnxruntime-node versions:
// ruflo/node_modules/onnxruntime-node (1.24.3)
// ruflo/node_modules/@xenova/transformers/node_modules/onnxruntime-node (1.14.0)
// Both ship onnxruntime.dll; loading both into one process => DLL name
// collision => ERR_DLOPEN_FAILED. Disabling the nested duplicate makes
// @xenova/transformers resolve to the single hoisted 1.24.3.
const DEDUPE_DISABLED_SUFFIX = '.h7disabled';
/** Resolve { active, disabled } paths for the duplicate nested onnxruntime-node. */
export function resolveDedupePaths(npmRoot) {
const active = join(
npmRoot, 'ruflo', 'node_modules', '@xenova', 'transformers',
'node_modules', 'onnxruntime-node',
);
return { active, disabled: active + DEDUPE_DISABLED_SUFFIX };
}
/**
* Dedupe status:
* 'deduped' — disabled dir exists, active does not (fix applied)
* 'not-deduped' — active dir exists, disabled does not (fix not applied)
* 'absent' — neither exists (no nested duplicate — nothing to dedupe)
* 'inconsistent' — both exist (manual intervention required)
*/
export function dedupeStatus(active, disabled) {
const a = existsSync(active);
const d = existsSync(disabled);
if (a && d) return 'inconsistent';
if (d) return 'deduped';
if (a) return 'not-deduped';
return 'absent';
}
/** Disable the nested duplicate (rename active -> disabled). Idempotent. */
export function applyDedupe(active, disabled) {
const status = dedupeStatus(active, disabled);
if (status === 'deduped' || status === 'absent') return { changed: false, status };
if (status === 'inconsistent') {
throw new Error(
`dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`,
);
}
renameSync(active, disabled);
return { changed: true, status: 'deduped' };
}
/** Re-enable the nested duplicate (rename disabled -> active). */
export function revertDedupe(active, disabled) {
const status = dedupeStatus(active, disabled);
if (status === 'not-deduped' || status === 'absent') return { changed: false, status };
if (status === 'inconsistent') {
throw new Error(
`dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`,
);
}
renameSync(disabled, active);
return { changed: true, status: 'not-deduped' };
}
function main() {
const mode = process.argv[2];
const VALID_MODES = [undefined, '--check', '--revert'];
if (!VALID_MODES.includes(mode)) {
console.error(`[ruflo-h7-patch] unknown flag: ${mode}`);
console.error('Usage: node tools/ruflo-h7-patch.mjs [--check | --revert]');
process.exit(2);
}
let npmRoot;
try {
npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
} catch (e) {
console.error('[ruflo-h7-patch] failed to run `npm root -g`: ' + e.message);
process.exit(2);
}
const target = resolveTargetPath(npmRoot);
if (!target) {
console.error('[ruflo-h7-patch] memory-initializer.js not found under ' + npmRoot);
process.exit(2);
}
const { active, disabled } = resolveDedupePaths(npmRoot);
if (mode === '--check') {
const patched = isPatched(readFileSync(target, 'utf8'));
const dStatus = dedupeStatus(active, disabled);
const dedupeOk = dStatus === 'deduped' || dStatus === 'absent';
console.log(`[ruflo-h7-patch] ${target}`);
console.log(` getBridge patch: ${patched ? 'PATCHED' : 'UNPATCHED'}`);
console.log(` onnxruntime dedupe: ${dStatus}`);
process.exit(patched && dedupeOk ? 0 : 1);
}
if (mode === '--revert') {
const r1 = revertPatch(readFileSync(target, 'utf8'));
if (r1.changed) writeFileSync(target, r1.content);
console.log(`[ruflo-h7-patch] revert getBridge: ${r1.changed ? 'patch removed' : 'was not patched'}`);
try {
const r2 = revertDedupe(active, disabled);
console.log(`[ruflo-h7-patch] revert dedupe: ${r2.changed ? 're-enabled nested onnxruntime-node' : r2.status}`);
} catch (e) {
console.error('[ruflo-h7-patch] ' + e.message);
process.exit(3);
}
process.exit(0);
}
// default: apply
try {
const r1 = applyPatch(readFileSync(target, 'utf8'));
if (r1.changed) writeFileSync(target, r1.content);
console.log(`[ruflo-h7-patch] apply getBridge: ${r1.changed ? 'patched' : 'already patched'}`);
const r2 = applyDedupe(active, disabled);
console.log(`[ruflo-h7-patch] apply dedupe: ${r2.changed ? 'disabled nested onnxruntime-node' : r2.status}`);
process.exit(0);
} catch (e) {
console.error('[ruflo-h7-patch] ' + e.message);
process.exit(3);
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}