fix: #2219 #2221 #2222 #2215 #2226 — memory/routing/statusline bug cluster (3.10.6) (#2227)

* fix: #2219 #2221 #2222 #2215 #2226 — memory/routing/statusline bug cluster (3.10.6)

Five reproducible bugs reported by external contributors (pacphi, casparml,
HF-teamdev), each verified against source and covered by a regression test.

#2219 (silent write loss, Node 24/26) — agentdb pins better-sqlite3 as an
  OPTIONAL dep at ^11.8.1, which has no Node 24/25/26 prebuild; the native
  build fails silently and AgentDB drops to a non-persistent backend. Override
  better-sqlite3 to >=12.8.0 (ships Node 20–26 prebuilds) in BOTH the root
  umbrella and the ruflo wrapper (#2112: root overrides don't reach the
  wrapper). New CI guard audit-better-sqlite3-override.mjs prevents regression.

#2226 (pattern store/search backend mismatch) — agentdb_pattern-store and
  agentdb_pattern-search hit disjoint backends, so stored patterns were never
  found. Two paths fixed: (a) when the ReasoningBank controller is present,
  bridgeSearchPatterns now reads LocalReasoningBank.findSimilar/getAll (the
  same backend store writes to); (b) when the controller is absent (the common
  real-world state), the memory-store-fallback search Tier-2 now hydrates entry
  content via getEntry — listEntries returns metadata only (see #2014), so the
  substring scan previously matched nothing.

#2222 (route feedback no-op) — `route feedback` applied the Q-update in memory
  but the CLI exits before the Q-learner's autoSaveInterval fires, so learning
  never persisted. Add an explicit awaited router.saveModel() after feedback.

#2221 (statusline shows V3.6 on global installs) — getPkgVersion() never probed
  the global npm root, so `npm i -g ruflo` fell back to the hard-coded default.
  Derive the global node_modules dir from process.execPath (no npm spawn) and
  probe it; covers nvm/mise and Windows layouts. Regenerated statusline.cjs.

#2215 (flashAttention contradictory state) — system_info emitted a hard-coded
  `flashAttention: false` while hooks_intelligence reported the live probe.
  system_info now runs the same getFlashAttention() probe as the authoritative
  path so the two tools can't disagree.

Tests: __tests__/bug-cluster-2219-2226.test.ts (5/5), statusline drift guard
still byte-identical (8/8), tsc clean, targeted sweep 120/120.

Co-Authored-By: RuFlo <ruv@ruv.net>

* fix(deps): #2219 — sync package-lock to better-sqlite3@12.10.0 override

npm ci validates lockfile against package.json; the override bumped the
resolved version but the committed lockfile still pinned 11.10.0, failing
'npm ci' in CI. Regenerated with --package-lock-only.

Co-Authored-By: RuFlo <ruv@ruv.net>
This commit is contained in:
rUv authored and GitHub committed 2026-05-29 08:52:31 -04:00
1 parent b8a49cfb82
commit bb34896656
13 files changed
+333 -27

No files matched your search

+14
View File
@@ -345,6 +345,20 @@ function getPkgVersion() {
path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
path.join(CWD, 'v3', '@claude-flow', 'cli', 'package.json'),
];
// #2221: global installs (npm i -g ruflo) live outside CWD/node_modules, so the
// probes above all miss and the version falls back to the hard-coded default.
// Derive the global node_modules dir from the running node binary (no npm spawn —
// statusline renders often). Covers nvm/mise (bin/../lib/node_modules) and Windows
// (bin/node_modules) layouts.
try {
const binDir = path.dirname(process.execPath);
for (const gm of [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')]) {
pkgPaths.push(
path.join(gm, 'ruflo', 'package.json'),
path.join(gm, '@claude-flow', 'cli', 'package.json'),
);
}
} catch { /* ignore */ }
for (const p of pkgPaths) {
if (!fs.existsSync(p)) continue;
try {
+11
View File
@@ -1618,6 +1618,17 @@ jobs:
shell: bash
run: node scripts/audit-umbrella-version-lockstep.mjs
- name: Run better-sqlite3 override audit (#2219)
# agentdb pins better-sqlite3 as an OPTIONAL dep at ^11.8.1, which has
# no Node 24/25/26 prebuild → the optional native build fails silently
# on those runtimes and AgentDB drops to a non-persistent backend
# (silent write loss). Asserts both the root umbrella and the ruflo
# wrapper override better-sqlite3 to >=12.8.0 (which ships Node 20–26
# prebuilds). Root overrides do NOT reach the published wrapper (#2112),
# so both must carry it.
shell: bash
run: node scripts/audit-better-sqlite3-override.mjs
- name: Run plugin-hooks cross-platform audit (#2132)
# Catches /bin/bash literals, POSIX-only pipelines (jq, xargs, tr),
# and .sh script invocations in plugin hooks.json files — patterns
+8 -20
View File
@@ -1,12 +1,12 @@
{
"name": "claude-flow",
"version": "3.7.0-alpha.80",
"version": "3.10.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-flow",
"version": "3.7.0-alpha.80",
"version": "3.10.6",
"license": "MIT",
"dependencies": {
"@claude-flow/cli-core": "^3.7.0-alpha.5",
@@ -230,21 +230,6 @@
"better-sqlite3": "^12.9.0"
}
},
"node_modules/@claude-flow/memory/node_modules/better-sqlite3": {
"version": "12.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
}
},
"node_modules/@claude-flow/neural": {
"version": "3.0.0-alpha.9",
"resolved": "https://registry.npmjs.org/@claude-flow/neural/-/neural-3.0.0-alpha.9.tgz",
@@ -4503,15 +4488,18 @@
"optional": true
},
"node_modules/better-sqlite3": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"version": "12.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
}
},
"node_modules/bignumber.js": {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-flow",
"version": "3.10.5",
"version": "3.10.6",
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
"main": "dist/index.js",
"type": "module",
@@ -75,6 +75,7 @@
"agentic-flow": "^2.0.13"
},
"overrides": {
"better-sqlite3": ">=12.8.0",
"hono": ">=4.11.4",
"@ruvector/rvf-wasm": "0.1.5",
"@hono/node-server": ">=1.19.10",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ruflo",
"version": "3.10.5",
"version": "3.10.6",
"description": "Ruflo - Enterprise AI agent orchestration platform. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
"main": "bin/ruflo.js",
"type": "module",
@@ -43,6 +43,7 @@
"@claude-flow/cli": "^3.10.3"
},
"overrides": {
"better-sqlite3": ">=12.8.0",
"@ruvector/rvf-wasm": "0.1.5",
"@hono/node-server": ">=1.19.10",
"flatted": ">=3.4.0",
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Static guard for ruvnet/ruflo#2219 — keep the `better-sqlite3` override
* pinned to a version that ships Node 24/25/26 prebuilds.
*
* Why: `agentdb` declares `better-sqlite3` as an OPTIONAL dependency at
* `^11.8.1`. better-sqlite3 11.8.1 has no prebuilt binary for Node 24/25/26,
* so on those runtimes the optional native build fails *silently* (optional
* deps never error) and AgentDB falls back to a non-persistent backend —
* users on Node 24/26 saw stores succeed but never persist (silent write
* loss). better-sqlite3 >=12.8.0 ships prebuilds for Node 20–26.
*
* Per the #2112 lesson, root overrides do NOT propagate to the published
* `ruflo` wrapper — the override MUST be present in BOTH the root umbrella
* (`package.json`) and the wrapper (`ruflo/package.json`). This guard asserts
* both carry `better-sqlite3 >= 12.8.0`. Wired into v3-ci.yml.
*
* Exit codes:
* 0 — both overrides present and satisfy >=12.8.0
* 1 — missing override, or a range that admits a Node-24/26-broken version
*/
import { readFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import semver from 'semver';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');
// The floor that has Node 24/25/26 prebuilds. Any override that could resolve
// below this re-opens the silent-write-loss on new Node majors.
const MIN_SAFE = '12.8.0';
const TARGETS = [
{ label: 'claude-flow (root umbrella)', path: 'package.json' },
{ label: 'ruflo (published wrapper)', path: 'ruflo/package.json' },
];
let failed = false;
for (const { label, path: rel } of TARGETS) {
const p = join(REPO_ROOT, rel);
if (!existsSync(p)) {
console.error(`✗ ${label}: ${rel} not found`);
failed = true;
continue;
}
let pkg;
try {
pkg = JSON.parse(readFileSync(p, 'utf8'));
} catch (e) {
console.error(`✗ ${label}: ${rel} is not valid JSON — ${e.message}`);
failed = true;
continue;
}
const range = pkg.overrides?.['better-sqlite3'];
if (!range) {
console.error(
`✗ ${label}: missing overrides["better-sqlite3"]. ` +
`Add "better-sqlite3": ">=${MIN_SAFE}" so agentdb's optional native ` +
`dep resolves to a version with Node 24/25/26 prebuilds (#2219).`,
);
failed = true;
continue;
}
// The minimum version the range can resolve to must be >= MIN_SAFE, so the
// override can never drop back to a Node-24/26-broken better-sqlite3.
const minOfRange = semver.minVersion(range);
if (!minOfRange || semver.lt(minOfRange, MIN_SAFE)) {
console.error(
`✗ ${label}: overrides["better-sqlite3"] = "${range}" can resolve below ` +
`${MIN_SAFE} (min ${minOfRange ?? 'unparseable'}). Node 24/25/26 need ` +
`>=${MIN_SAFE} prebuilds (#2219).`,
);
failed = true;
continue;
}
console.log(`✓ ${label}: better-sqlite3 override "${range}" (min ${minOfRange}) ≥ ${MIN_SAFE}`);
}
if (failed) {
console.error('\nbetter-sqlite3 override guard FAILED (#2219). See messages above.');
process.exit(1);
}
console.log('\n✓ better-sqlite3 override guard passed — Node 24/25/26 persistence protected.');
process.exit(0);
@@ -0,0 +1,115 @@
/**
* Regression tests for the 2026-05-29 bug cluster (issues #2215, #2221, #2222, #2226).
*
* Each test reproduces a reported defect and asserts the fix holds:
* #2215 — system_info and hooks_intelligence must agree on flashAttention state.
* #2221 — the generated statusline probes the global npm install for its version.
* #2222 — `route feedback` persists the Q-table update to disk (no longer a no-op).
* #2226 — agentdb_pattern-store and agentdb_pattern-search share a backend
* (a stored pattern is findable by search).
*
* Backend-dependent tests degrade gracefully (skip the assertion) when the
* runtime backend cannot initialize in isolation, matching the existing
* statusline drift-guard convention.
*/
import { describe, it, expect } from 'vitest';
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import { generateStatuslineScript } from '../src/init/statusline-generator.js';
import { DEFAULT_INIT_OPTIONS } from '../src/init/types.js';
import { systemTools } from '../src/mcp-tools/system-tools.js';
import { hooksTools } from '../src/mcp-tools/hooks-tools.js';
import { agentdbPatternStore, agentdbPatternSearch } from '../src/mcp-tools/agentdb-tools.js';
import { createQLearningRouter } from '../src/ruvector/q-learning-router.js';
function findTool(tools: any[], name: string) {
const t = tools.find((x) => x.name === name);
if (!t) throw new Error(`tool ${name} not found`);
return t;
}
describe('#2221 — statusline version probe covers global npm installs', () => {
const SCRIPT = generateStatuslineScript(DEFAULT_INIT_OPTIONS);
it('derives the global node_modules dir from process.execPath (no npm spawn)', () => {
expect(SCRIPT).toContain('process.execPath');
expect(SCRIPT).toContain("'lib', 'node_modules'");
});
it('still keeps the project-local and plugin-marketplace probes', () => {
expect(SCRIPT).toContain("'marketplaces', 'ruflo', 'package.json'");
expect(SCRIPT).toContain("'node_modules', 'ruflo', 'package.json'");
});
});
describe('#2215 — system_info and hooks_intelligence agree on flashAttention', () => {
it('reports the SAME boolean for flashAttention', async () => {
const sysInfo = await findTool(systemTools, 'system_info').handler({});
const intel = await findTool(hooksTools, 'hooks_intelligence').handler({ showStatus: true });
const sysFlash = sysInfo.features.flashAttention;
expect(typeof sysFlash).toBe('boolean');
// hooks_intelligence exposes the authoritative runtime status; both must match.
const comp = intel?.components?.flashAttention;
if (!comp) return; // intelligence registry unavailable in isolation — skip
const intelFlash = comp.status === 'active';
expect(sysFlash).toBe(intelFlash);
});
});
describe('#2222 — route feedback persists the Q-table to disk', () => {
it('writes the model after a single update + explicit save', async () => {
const dir = mkdtempSync(path.join(tmpdir(), 'ruflo-qlearn-'));
const modelPath = path.join(dir, 'q-learning-model.json');
try {
const router = createQLearningRouter({ modelPath, autoSaveInterval: 100 });
await router.initialize();
expect(existsSync(modelPath)).toBe(false); // nothing saved yet
// Mirror the FIXED feedback handler: one update, then explicit awaited save.
router.update('implement auth', 'coder', -1);
const persisted = await router.saveModel();
expect(persisted).toBe(true);
expect(existsSync(modelPath)).toBe(true);
const model = JSON.parse(readFileSync(modelPath, 'utf-8'));
expect(model.stats.updateCount).toBeGreaterThanOrEqual(1);
expect(Object.keys(model.qTable).length).toBeGreaterThan(0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe('#2226 — pattern store and search share a backend', () => {
it('stores a pattern and finds it via search', async () => {
const marker = `oauth-refresh-token-rotation-${process.pid}-${process.hrtime.bigint()}`;
// Backend init (registry + ONNX embeddings) can be slow on first load.
const stored = await agentdbPatternStore.handler({
pattern: `Use ${marker} for secure session renewal`,
type: 'auth-pattern',
confidence: 0.9,
});
if (!stored || stored.success !== true) return; // backend unavailable in isolation — skip
const found = await agentdbPatternSearch.handler({
query: marker,
topK: 5,
minConfidence: 0.1,
});
expect(Array.isArray(found.results)).toBe(true);
// Result text lives in `content` (reasoningBank path) or `pattern` (fallback path).
const hit = found.results.find((r: any) => {
const text = typeof r.content === 'string' ? r.content
: typeof r.pattern === 'string' ? r.pattern : '';
return text.includes(marker);
});
expect(hit).toBeDefined();
}, 60_000);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@claude-flow/cli",
"version": "3.10.5",
"version": "3.10.6",
"type": "module",
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
"main": "dist/src/index.js",
+10
View File
@@ -438,7 +438,17 @@ const feedbackCommand: Command = {
const clampedReward = Math.max(-1, Math.min(1, reward));
const tdError = router.update(taskDescription, agentId, clampedReward, nextTask);
// #2222: persist immediately. The Q-learner only auto-saves every
// autoSaveInterval updates, but the CLI process exits after this single
// feedback call, so without an explicit flush the learned update is lost
// and route-learning never improves across invocations. saveModel()
// swallows its own errors (returns false), so this await cannot throw.
const persisted = await router.saveModel();
output.printSuccess(`Feedback recorded for agent "${agent.name}"`);
if (!persisted) {
output.printWarning('Feedback applied but the model could not be persisted to disk.');
}
output.writeln();
output.printBox([
`Task: ${taskDescription}`,
@@ -372,6 +372,20 @@ function getPkgVersion() {
path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
path.join(CWD, 'v3', '@claude-flow', 'cli', 'package.json'),
];
// #2221: global installs (npm i -g ruflo) live outside CWD/node_modules, so the
// probes above all miss and the version falls back to the hard-coded default.
// Derive the global node_modules dir from the running node binary (no npm spawn —
// statusline renders often). Covers nvm/mise (bin/../lib/node_modules) and Windows
// (bin/node_modules) layouts.
try {
const binDir = path.dirname(process.execPath);
for (const gm of [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')]) {
pkgPaths.push(
path.join(gm, 'ruflo', 'package.json'),
path.join(gm, '@claude-flow', 'cli', 'package.json'),
);
}
} catch { /* ignore */ }
for (const p of pkgPaths) {
if (!fs.existsSync(p)) continue;
try {
@@ -205,7 +205,7 @@ export const agentdbPatternSearch: MCPTool = {
// against each entry's pattern text. Deterministic; survives
// embedding-index latency and threshold tuning.
try {
const { searchEntries, listEntries } = await import('../memory/memory-initializer.js');
const { searchEntries, listEntries, getEntry } = await import('../memory/memory-initializer.js');
const parseEntry = (e: Record<string, unknown>): Record<string, unknown> | null => {
const raw = typeof e.content === 'string' ? e.content : (e as { value?: unknown }).value;
@@ -236,14 +236,29 @@ export const agentdbPatternSearch: MCPTool = {
.filter((r): r is Record<string, unknown> => r !== null);
} catch { /* fall through to tier 2 */ }
// Tier 2 — substring scan (catches just-written entries before HNSW indexes them)
// Tier 2 — substring scan (catches just-written entries before HNSW indexes them).
// #2226: listEntries returns metadata only (no content/value — see open #2014),
// so parseEntry would always null out here. Fetch each entry's content by key via
// getEntry (which DOES return content) before parsing/matching. This is the path
// that actually runs when the ReasoningBank controller is unavailable (the common
// real-world state), so a stored pattern is now findable by search.
if (results.length === 0) {
tier = 'substring';
const all = await listEntries({ namespace: 'pattern', limit: 200 });
const qLower = query.toLowerCase();
const matched: Array<Record<string, unknown>> = [];
for (const e of (all?.entries ?? [])) {
const parsed = parseEntry(e as Record<string, unknown>);
const meta = e as Record<string, unknown>;
let entry: Record<string, unknown> = meta;
// If the listing lacks content, hydrate it from the keyed store.
if (typeof meta.content !== 'string' && typeof (meta as { value?: unknown }).value !== 'string') {
const key = typeof meta.key === 'string' ? meta.key : (typeof meta.id === 'string' ? meta.id : null);
if (!key) continue;
const got = await getEntry({ key, namespace: 'pattern' });
if (!got?.found || !got.entry) continue;
entry = got.entry as unknown as Record<string, unknown>;
}
const parsed = parseEntry(entry);
if (!parsed) continue;
const text = typeof parsed.pattern === 'string' ? parsed.pattern.toLowerCase() : '';
if (text.includes(qLower)) matched.push(parsed);
@@ -497,6 +497,17 @@ export const systemTools: MCPTool[] = [
},
},
handler: async () => {
// #2215: flashAttention must reflect the runtime probe, not a stale literal.
// Same source-of-truth as hooks_intelligence / neural_status so the tools
// can never report contradictory state for the same daemon.
let flashAttentionAvailable = false;
try {
const { getFlashAttention } = await import('@claude-flow/neural');
flashAttentionAvailable = getFlashAttention() !== null;
} catch {
flashAttentionAvailable = false;
}
return {
version: PKG_VERSION,
nodeVersion: process.version,
@@ -511,7 +522,7 @@ export const systemTools: MCPTool[] = [
neural: true,
hnsw: true,
quantization: true,
flashAttention: false,
flashAttention: flashAttentionAvailable,
},
limits: {
maxAgents: 100,
@@ -1473,6 +1473,42 @@ export async function bridgeSearchPatterns(options: {
};
}
// #2226 — the wired-in LocalReasoningBank implements store() + findSimilar()/getAll()
// but NOT searchPatterns()/search(). bridgeStorePattern commits patterns to its
// store(), so search MUST read the SAME backend or stored patterns are never found
// (previously search fell through to the disjoint sql.js 'pattern' namespace, which
// the store never wrote to → always-empty results). Adapt findSimilar (semantic) with
// a getAll() substring fallback so freshly-stored patterns are visible. This mirrors
// what hooks_intelligence_pattern-search already does against the same backend.
if (reasoningBank && typeof reasoningBank.findSimilar === 'function') {
const k = options.topK || 5;
const threshold = options.minConfidence ?? 0.3;
let mapped: Array<{ id: string; content: string; score: number }> = [];
try {
const { generateEmbedding } = await import('./memory-initializer.js');
const qEmb = await generateEmbedding(options.query);
if (qEmb && Array.isArray(qEmb.embedding) && qEmb.embedding.length > 0) {
const hits = reasoningBank.findSimilar(qEmb.embedding, { k, threshold });
mapped = (Array.isArray(hits) ? hits : []).map((r: any) => ({
id: r.id ?? '',
content: r.content ?? '',
score: r.confidence ?? r.score ?? 0,
}));
}
} catch { /* embedding unavailable — fall through to substring scan */ }
// Deterministic substring fallback over the same in-memory store.
if (mapped.length === 0 && typeof reasoningBank.getAll === 'function') {
const q = options.query.toLowerCase();
mapped = (reasoningBank.getAll() as any[])
.filter((p: any) => typeof p.content === 'string' && p.content.toLowerCase().includes(q))
.slice(0, k)
.map((p: any) => ({ id: p.id ?? '', content: p.content ?? '', score: p.confidence ?? 0 }));
}
return { results: mapped, controller: 'reasoningBank' };
}
// Fallback: search via bridge
const result = await bridgeSearchEntries({
query: options.query,