@claude-flow/cli 3.38.6 → 3.38.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.38.6",
3
+ "version": "3.38.8",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "85fe05c757421c52137c0bc8545a0896bab6b4714538c2a11d1d0835bfcc8c1c",
6
6
  "hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
7
- "intelligence.cjs": "66d63fdeab5f2ce0546bff80e69144911508fa2bf88f7b167ba0905762ecb314",
7
+ "intelligence.cjs": "30e42ed7ec4ca5a94ac54fdb1330d2d47ac5f3fdeeef207753574723a9e77b5c",
8
8
  "statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
9
9
  }
10
10
  },
11
- "signature": "0u8qz9OxORssiGc4kWWJR9fL9OPo7GZj6K7Zhkq+40XkKCP7nxLHx8LPcUwxhRHzVnnBtnE34TvsoTueM2VIBA==",
11
+ "signature": "EoEEuSnNwaVCAmnwd7Evq+V2sD0Zz5cOqqZO4PecBOchzrcimyqFqkz4eOyhYF6iIhEtQ8+XsOvZKFKMbtKIDg==",
12
12
  "algorithm": "ed25519"
13
13
  }
@@ -830,11 +830,16 @@ function consolidate() {
830
830
  pageRanks = computePageRank(nodes, edges, 0.85, 30);
831
831
  }
832
832
 
833
- // 6. Write updated graph
833
+ // 6. Write updated graph. #2920 follow-up: include contentFingerprint so
834
+ // init()'s cache-hit gate (line ~511) doesn't unconditionally miss on the
835
+ // very next init after a consolidate — without this, nodeCount alone
836
+ // matched but contentFingerprint was undefined here vs a real hash in
837
+ // init()'s own write, forcing a full rebuild every time.
834
838
  writeJSON(GRAPH_PATH, {
835
839
  version: 1,
836
840
  updatedAt: Date.now(),
837
841
  nodeCount: Object.keys(nodes).length,
842
+ contentFingerprint: storeFingerprint(store),
838
843
  nodes,
839
844
  edges,
840
845
  pageRanks,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 4,
4
- "generatedAt": "2026-08-12T20:46:09.024Z",
5
- "gitSha": "f7672a10",
4
+ "generatedAt": "2026-08-12T22:31:25.682Z",
5
+ "gitSha": "5efd5937",
6
6
  "catalog": {
7
7
  "agents": 164,
8
8
  "tools": 397,
@@ -27,8 +27,8 @@ export interface AgentRecord {
27
27
  * falling back to MODEL_MAP[tier].
28
28
  */
29
29
  modelId?: string;
30
- /** ADR-148 phase 2 — execution provider hint. */
31
- provider?: 'anthropic' | 'openrouter';
30
+ /** ADR-148 phase 2 — execution provider hint. #2962 widened to include 'ollama'. */
31
+ provider?: 'anthropic' | 'openrouter' | 'ollama';
32
32
  /** ADR-148 phase 2 — concrete OpenRouter slug when provider='openrouter'. */
33
33
  openrouterModel?: string;
34
34
  lastResult?: Record<string, unknown>;
@@ -42,6 +42,14 @@ export interface AnthropicCallInput {
42
42
  maxTokens?: number;
43
43
  temperature?: number;
44
44
  timeoutMs?: number;
45
+ /**
46
+ * #2962 — explicit provider carried from the agent record (agent.provider,
47
+ * itself populated from a user's `--provider` flag or persisted
48
+ * `providers configure`). When set, this outranks the env-var-only
49
+ * inference callAnthropicMessages otherwise does — see the precedence
50
+ * comment on that function.
51
+ */
52
+ provider?: 'anthropic' | 'openrouter' | 'ollama';
45
53
  }
46
54
  export interface AnthropicCallResult {
47
55
  success: boolean;
@@ -10,6 +10,7 @@
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { getProjectCwd } from './types.js';
13
+ import { configManager } from '../services/config-file-manager.js';
13
14
  const STORAGE_DIR = '.claude-flow';
14
15
  const AGENT_DIR = 'agents';
15
16
  const AGENT_FILE = 'store.json';
@@ -57,6 +58,29 @@ const MODEL_MAP = {
57
58
  export function modelRejectsSamplingParams(model) {
58
59
  return /^claude-(fable-5|opus-4-8|opus-4-7|sonnet-5)/.test(model);
59
60
  }
61
+ /**
62
+ * #2962 — read the enabled `agents.providers` entry matching `name` from
63
+ * cwd-scoped `claude-flow.config.json` (written by `providers configure`,
64
+ * `commands/providers.ts`). Best-effort: any failure (no config file, wrong
65
+ * shape, etc.) returns undefined rather than throwing — this must never
66
+ * break execution, and env vars remain a fully-supported override that
67
+ * doesn't require this file to exist.
68
+ */
69
+ function getPersistedProviderConfig(name) {
70
+ try {
71
+ const providers = configManager.get(getProjectCwd(), 'agents.providers');
72
+ if (!Array.isArray(providers))
73
+ return undefined;
74
+ const entry = providers.find((p) => !!p && typeof p === 'object' && typeof p.name === 'string' &&
75
+ p.name.toLowerCase() === name.toLowerCase());
76
+ if (!entry || entry.enabled === false)
77
+ return undefined;
78
+ return entry;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
60
84
  /**
61
85
  * Generic Anthropic Messages API call. No agent registry coupling — used
62
86
  * by agent_execute (with the agent's configured model) and by the WASM
@@ -69,7 +93,13 @@ export function modelRejectsSamplingParams(model) {
69
93
  * don't need to know which provider answered.
70
94
  */
71
95
  export async function callAnthropicMessages(input) {
72
- const explicitProvider = (process.env.RUFLO_PROVIDER || '').toLowerCase();
96
+ // #2962 precedence: explicit per-agent flag (input.provider, forwarded
97
+ // from agent.provider by executeAgentTask, itself populated from a
98
+ // user's `agent spawn --provider` flag) → env vars (RUFLO_PROVIDER + the
99
+ // *_API_KEY family — unchanged back-compat surface) → persisted
100
+ // `agents.providers` config (`providers configure`) → the original
101
+ // key-presence inference, kept below as the last-resort fallback.
102
+ const explicitProvider = (input.provider || process.env.RUFLO_PROVIDER || '').toLowerCase();
73
103
  const ollamaKey = process.env.OLLAMA_API_KEY;
74
104
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
75
105
  // #2042 — OpenRouter is an OpenAI-compat endpoint that fronts dozens of
@@ -80,22 +110,47 @@ export async function callAnthropicMessages(input) {
80
110
  // branch above).
81
111
  const openrouterKey = process.env.OPENROUTER_API_KEY;
82
112
  const useOpenRouter = explicitProvider === 'openrouter' || (!anthropicKey && !!openrouterKey);
83
- const useOllama = explicitProvider === 'ollama' || (!anthropicKey && !!ollamaKey && !openrouterKey);
84
- if (useOpenRouter && openrouterKey) {
85
- return callOpenAICompat({
86
- ...input,
87
- apiKey: openrouterKey,
88
- baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api',
89
- providerLabel: 'openrouter',
90
- // #2357 Finding C: anthropic/claude-3.5-sonnet was retired Oct 2025.
91
- // Default to the same canonical family the rest of the resolver uses
92
- // (MODEL_MAP). `OPENROUTER_DEFAULT_MODEL` still wins for callers who
93
- // want to pin a specific OpenRouter slug.
94
- defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || 'anthropic/claude-sonnet-4-6',
95
- });
113
+ // #2962 only consult the persisted config when a candidate is actually
114
+ // relevant (explicit choice, or no env key found anywhere), so a normal
115
+ // ANTHROPIC_API_KEY-only setup never pays a config-file read.
116
+ const persistedOllama = explicitProvider === 'ollama' || (!anthropicKey && !openrouterKey && !ollamaKey)
117
+ ? getPersistedProviderConfig('ollama')
118
+ : undefined;
119
+ const persistedOpenRouter = explicitProvider === 'openrouter' && !openrouterKey ? getPersistedProviderConfig('openrouter') : undefined;
120
+ const useOllama = explicitProvider === 'ollama' || (!anthropicKey && !openrouterKey && (!!ollamaKey || !!persistedOllama));
121
+ if (useOpenRouter) {
122
+ const apiKey = openrouterKey || persistedOpenRouter?.apiKey;
123
+ if (apiKey) {
124
+ return callOpenAICompat({
125
+ ...input,
126
+ apiKey,
127
+ baseUrl: process.env.OPENROUTER_BASE_URL || persistedOpenRouter?.baseUrl || 'https://openrouter.ai/api',
128
+ providerLabel: 'openrouter',
129
+ // #2357 Finding C: anthropic/claude-3.5-sonnet was retired Oct 2025.
130
+ // Default to the same canonical family the rest of the resolver uses
131
+ // (MODEL_MAP). `OPENROUTER_DEFAULT_MODEL` still wins for callers who
132
+ // want to pin a specific OpenRouter slug.
133
+ defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || persistedOpenRouter?.model || 'anthropic/claude-sonnet-4-6',
134
+ });
135
+ }
96
136
  }
97
- if (useOllama && ollamaKey) {
98
- return callOllamaCompat({ ...input, apiKey: ollamaKey });
137
+ if (useOllama) {
138
+ // #2962 retire the undocumented OLLAMA_API_KEY=local sentinel
139
+ // requirement. A self-hosted, unauthenticated Ollama daemon shouldn't
140
+ // need a fake credential to become reachable; "self-hosted" is any
141
+ // resolved base URL that isn't the public Ollama Cloud endpoint
142
+ // (persisted `providers configure -e` baseUrl, or OLLAMA_BASE_URL,
143
+ // checked in that order — matching callOllamaCompat's own precedence).
144
+ const resolvedBaseUrl = process.env.OLLAMA_BASE_URL || persistedOllama?.baseUrl;
145
+ const isSelfHosted = !!resolvedBaseUrl && !/^https:\/\/ollama\.com\/?$/i.test(resolvedBaseUrl);
146
+ if (ollamaKey || persistedOllama?.apiKey || isSelfHosted) {
147
+ return callOllamaCompat({
148
+ ...input,
149
+ apiKey: ollamaKey || persistedOllama?.apiKey || 'local',
150
+ baseUrl: resolvedBaseUrl,
151
+ model: input.model || persistedOllama?.model,
152
+ });
153
+ }
99
154
  }
100
155
  if (!anthropicKey) {
101
156
  return {
@@ -185,15 +240,18 @@ export async function callAnthropicMessages(input) {
185
240
  async function callOllamaCompat(input) {
186
241
  const model = resolveOllamaModel(input.model);
187
242
  const startedAt = Date.now();
188
- // OLLAMA_BASE_URL lets users point at local/self-hosted endpoints
189
- // (e.g. http://ruvultra:11434, http://localhost:11434) instead of
190
- // Ollama Cloud. Default is the public cloud endpoint.
191
- const base = (process.env.OLLAMA_BASE_URL || 'https://ollama.com').replace(/\/+$/, '');
243
+ // #2962 input.baseUrl (resolved by the caller from persisted
244
+ // `providers configure` config, then OLLAMA_BASE_URL) takes precedence
245
+ // over re-reading OLLAMA_BASE_URL here, so a persisted-config-only setup
246
+ // (no env vars) still reaches a self-hosted endpoint instead of Ollama
247
+ // Cloud. Falls back to the original OLLAMA_BASE_URL-or-cloud behavior
248
+ // for any direct caller that doesn't pass baseUrl.
249
+ const base = (input.baseUrl || process.env.OLLAMA_BASE_URL || 'https://ollama.com').replace(/\/+$/, '');
192
250
  const url = `${base}/v1/chat/completions`;
193
251
  // Self-hosted endpoints typically don't need an Authorization header
194
252
  // (the daemon binds to 11434 with no auth by default), but Ollama Cloud
195
253
  // does. Send the bearer when the key is non-empty AND looks cloud-shaped.
196
- const sendAuth = input.apiKey && input.apiKey !== 'local';
254
+ const sendAuth = !!input.apiKey && input.apiKey !== 'local';
197
255
  try {
198
256
  const controller = new AbortController();
199
257
  const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
@@ -406,6 +464,11 @@ export async function executeAgentTask(input) {
406
464
  const startedAt = Date.now();
407
465
  // #2042 — delegate to callAnthropicMessages so the v3 provider router
408
466
  // (Anthropic / Ollama / OpenRouter) governs which backend is hit.
467
+ // #2962 — forward the agent's own explicit provider (set at spawn time
468
+ // from --provider / persisted config) so it outranks env-var inference.
469
+ // Only the first-attempt call; the ADR-149 fallback-retry call below is
470
+ // driven by the cost-optimal neural router picking model-id alternatives,
471
+ // an orthogonal mechanism left as-is to avoid unintended interaction.
409
472
  let result = await callAnthropicMessages({
410
473
  model: anthropicModel,
411
474
  prompt: input.prompt,
@@ -413,6 +476,7 @@ export async function executeAgentTask(input) {
413
476
  maxTokens: input.maxTokens,
414
477
  temperature: input.temperature,
415
478
  timeoutMs: input.timeoutMs,
479
+ provider: agent.provider,
416
480
  });
417
481
  // ADR-149 iter 7 — fallback chain on retryable failures (429, 5xx,
418
482
  // timeout). When the cost-optimal neural backend picked a specific
@@ -126,8 +126,19 @@ async function embedTaskSafe(task) {
126
126
  */
127
127
  async function determineAgentModel(agentType, config, task) {
128
128
  // 1. Explicit model in config
129
- if (config.model && ['haiku', 'sonnet', 'opus', 'opus-4.7', 'inherit'].includes(config.model)) {
130
- return { model: config.model, routedBy: 'explicit' };
129
+ if (config.model) {
130
+ const explicitModel = config.model;
131
+ if (['haiku', 'sonnet', 'opus', 'opus-4.7', 'inherit'].includes(explicitModel)) {
132
+ return { model: explicitModel, routedBy: 'explicit' };
133
+ }
134
+ // #2962 — a non-alias model string (e.g. an Ollama tag like
135
+ // 'qwen3.6:27b', or any other provider-native model id) is still an
136
+ // explicit user selection. Route it through the modelId fast-path
137
+ // (executeAgentTask already prefers agent.modelId over
138
+ // MODEL_MAP[agent.model] — ADR-149 iter 13) instead of falling through
139
+ // to task-based routing / agent-type defaults, which silently
140
+ // substituted 'sonnet' and discarded the user's request.
141
+ return { model: 'sonnet', routedBy: 'explicit', modelId: explicitModel };
131
142
  }
132
143
  // 2. Enhanced task-based routing with deterministic Tier-1 codemods
133
144
  if (task) {
@@ -248,6 +259,15 @@ export const agentTools = [
248
259
  const task = input.task || config.task || undefined;
249
260
  // Determine model using ADR-026 3-tier routing logic
250
261
  const routingResult = await determineAgentModel(agentType, config, task);
262
+ // #2962 — an explicit, unambiguous provider choice in config wins over
263
+ // the router's own pick. 'anthropic' is deliberately excluded: the CLI's
264
+ // `agent spawn` action always sets config.provider (defaulting to
265
+ // 'anthropic' when --provider isn't passed — commands/agent.ts), so
266
+ // 'anthropic' here is indistinguishable from that silent default.
267
+ // 'ollama'/'openrouter' are never silently defaulted and are unambiguous.
268
+ const explicitConfigProvider = config.provider === 'ollama' || config.provider === 'openrouter'
269
+ ? config.provider
270
+ : undefined;
251
271
  const agent = {
252
272
  agentId,
253
273
  agentType,
@@ -260,7 +280,9 @@ export const agentTools = [
260
280
  model: routingResult.model,
261
281
  modelRoutedBy: routingResult.routedBy,
262
282
  ...(routingResult.modelId ? { modelId: routingResult.modelId } : {}),
263
- ...(routingResult.provider ? { provider: routingResult.provider } : {}),
283
+ ...(explicitConfigProvider
284
+ ? { provider: explicitConfigProvider }
285
+ : routingResult.provider ? { provider: routingResult.provider } : {}),
264
286
  ...(routingResult.openrouterModel ? { openrouterModel: routingResult.openrouterModel } : {}),
265
287
  };
266
288
  store.agents[agentId] = agent;
@@ -331,7 +353,11 @@ export const agentTools = [
331
353
  model: agent.model,
332
354
  modelRoutedBy: routingResult.routedBy,
333
355
  ...(routingResult.modelId ? { modelId: routingResult.modelId } : {}),
334
- ...(routingResult.provider ? { provider: routingResult.provider } : {}),
356
+ // #2962 mirror the same precedence used for the stored agent
357
+ // record, so the response a caller sees matches what was actually
358
+ // persisted (previously this always reported the router's own
359
+ // pick, silently dropping an explicit config.provider override).
360
+ ...(agent.provider ? { provider: agent.provider } : {}),
335
361
  ...(routingResult.openrouterModel ? { openrouterModel: routingResult.openrouterModel } : {}),
336
362
  status: 'registered',
337
363
  createdAt: agent.createdAt,
@@ -203,6 +203,23 @@ async function getMemoryFunctions() {
203
203
  checkMemoryInitialization,
204
204
  };
205
205
  }
206
+ /**
207
+ * #2922: every `backend:` field in this file's tool responses used to be the
208
+ * hardcoded literal `'sql.js + HNSW'` regardless of which search path was
209
+ * actually active — most of the time that's the bridge doing a brute-force
210
+ * cosine scan, not HNSW. This reports the real algorithm via the same
211
+ * capability probe `getHNSWStatus()` already exposes.
212
+ */
213
+ async function describeBackend() {
214
+ try {
215
+ const { getHNSWStatus } = await import('../memory/memory-initializer.js');
216
+ const status = getHNSWStatus();
217
+ return status.algorithm === 'hnsw' ? 'sql.js + HNSW' : 'sqlite (bridge, brute-force cosine)';
218
+ }
219
+ catch {
220
+ return 'sqlite';
221
+ }
222
+ }
206
223
  /**
207
224
  * Ensure memory database is initialized and migrate legacy data if needed.
208
225
  * #1606: Wrapped in try/catch to prevent process-level crashes that kill
@@ -318,7 +335,7 @@ export const memoryTools = [
318
335
  hasEmbedding: !!result.embedding,
319
336
  embeddingDimensions: result.embedding?.dimensions || null,
320
337
  provenanceType: provenanceType || 'unknown',
321
- backend: 'sql.js + HNSW',
338
+ backend: await describeBackend(),
322
339
  storeTime: `${duration.toFixed(2)}ms`,
323
340
  error: result.error,
324
341
  };
@@ -371,7 +388,7 @@ export const memoryTools = [
371
388
  accessCount: result.entry.accessCount,
372
389
  hasEmbedding: result.entry.hasEmbedding,
373
390
  found: true,
374
- backend: 'sql.js + HNSW',
391
+ backend: await describeBackend(),
375
392
  };
376
393
  }
377
394
  return {
@@ -584,7 +601,7 @@ export const memoryTools = [
584
601
  namespace,
585
602
  deleted: result.deleted,
586
603
  hnswIndexInvalidated: result.deleted,
587
- backend: 'sql.js + HNSW',
604
+ backend: await describeBackend(),
588
605
  };
589
606
  }
590
607
  catch (error) {
@@ -641,7 +658,7 @@ export const memoryTools = [
641
658
  total: result.total,
642
659
  limit,
643
660
  offset,
644
- backend: 'sql.js + HNSW',
661
+ backend: await describeBackend(),
645
662
  };
646
663
  }
647
664
  catch (error) {
@@ -685,7 +702,7 @@ export const memoryTools = [
685
702
  ? `${((withEmbeddings / allEntries.total) * 100).toFixed(1)}%`
686
703
  : '0%',
687
704
  namespaces,
688
- backend: 'sql.js + HNSW',
705
+ backend: await describeBackend(),
689
706
  version: status.version || '3.0.0',
690
707
  features: status.features || {
691
708
  vectorEmbeddings: true,
@@ -736,7 +753,7 @@ export const memoryTools = [
736
753
  success: true,
737
754
  message: 'Migration completed',
738
755
  migrated: Object.keys(legacyStore.entries).length,
739
- backend: 'sql.js + HNSW',
756
+ backend: await describeBackend(),
740
757
  };
741
758
  },
742
759
  },
@@ -1149,7 +1166,7 @@ export const memoryTools = [
1149
1166
  bytes += e.size || 0;
1150
1167
  }
1151
1168
  return {
1152
- backend: 'sql.js + HNSW',
1169
+ backend: await describeBackend(),
1153
1170
  entries: all.total ?? all.entries.length,
1154
1171
  size: bytes,
1155
1172
  namespaces: Object.entries(nsCounts).map(([name, entries]) => ({ name, entries })),
@@ -209,21 +209,29 @@ export declare function bridgeLoadEmbeddingModel(dbPath?: string): Promise<{
209
209
  loadTime?: number;
210
210
  } | null>;
211
211
  /**
212
- * Get HNSW status from AgentDB v3's vector backend or HNSW index.
212
+ * Get vector search status from AgentDB v3's SQLite-backed store.
213
213
  * Returns null if unavailable.
214
- */
215
- export declare function bridgeGetHNSWStatus(dbPath?: string): Promise<{
214
+ *
215
+ * #2922: previously named `bridgeGetHNSWStatus` and unconditionally returned
216
+ * `available: true` whenever a DB connection succeeded — but the search this
217
+ * status describes (`bridgeSearchBruteForceCosine`, below) is a full-table
218
+ * SELECT + brute-force cosine scan, not an HNSW index lookup. Renamed and
219
+ * given an explicit `algorithm` field so callers can't mistake "vector
220
+ * search works" for "vector search is HNSW-accelerated".
221
+ */
222
+ export declare function bridgeGetVectorSearchStatus(dbPath?: string): Promise<{
216
223
  available: boolean;
217
224
  initialized: boolean;
218
225
  entryCount: number;
219
226
  dimensions: number;
227
+ algorithm: 'brute-force-cosine';
220
228
  } | null>;
221
229
  /**
222
- * Search using AgentDB v3's embedder + SQLite entries.
223
- * This is the HNSW-equivalent search through the bridge.
224
- * Returns null if unavailable.
230
+ * Search AgentDB v3's embedder + SQLite entries via a full-table scan and
231
+ * brute-force cosine similarity. NOT HNSW-accelerated see #2922. Returns
232
+ * null if unavailable.
225
233
  */
226
- export declare function bridgeSearchHNSW(queryEmbedding: number[], options?: {
234
+ export declare function bridgeSearchBruteForceCosine(queryEmbedding: number[], options?: {
227
235
  k?: number;
228
236
  namespace?: string;
229
237
  threshold?: number;
@@ -235,10 +243,11 @@ export declare function bridgeSearchHNSW(queryEmbedding: number[], options?: {
235
243
  namespace: string;
236
244
  }> | null>;
237
245
  /**
238
- * Add entry to the bridge's database with embedding.
239
- * Returns null if unavailable.
246
+ * Add entry to the bridge's database with embedding. No HNSW index is built
247
+ * or updated here — see #2922; the embedding is only stored for a later
248
+ * brute-force scan. Returns null if unavailable.
240
249
  */
241
- export declare function bridgeAddToHNSW(id: string, embedding: number[], entry: {
250
+ export declare function bridgeAddEmbedding(id: string, embedding: number[], entry: {
242
251
  id: string;
243
252
  key: string;
244
253
  namespace: string;
@@ -1478,10 +1478,17 @@ export async function bridgeLoadEmbeddingModel(dbPath) {
1478
1478
  }
1479
1479
  // ===== Phase 3: HNSW bridge =====
1480
1480
  /**
1481
- * Get HNSW status from AgentDB v3's vector backend or HNSW index.
1481
+ * Get vector search status from AgentDB v3's SQLite-backed store.
1482
1482
  * Returns null if unavailable.
1483
+ *
1484
+ * #2922: previously named `bridgeGetHNSWStatus` and unconditionally returned
1485
+ * `available: true` whenever a DB connection succeeded — but the search this
1486
+ * status describes (`bridgeSearchBruteForceCosine`, below) is a full-table
1487
+ * SELECT + brute-force cosine scan, not an HNSW index lookup. Renamed and
1488
+ * given an explicit `algorithm` field so callers can't mistake "vector
1489
+ * search works" for "vector search is HNSW-accelerated".
1483
1490
  */
1484
- export async function bridgeGetHNSWStatus(dbPath) {
1491
+ export async function bridgeGetVectorSearchStatus(dbPath) {
1485
1492
  const registry = await getRegistry(dbPath);
1486
1493
  if (!registry)
1487
1494
  return null;
@@ -1503,6 +1510,7 @@ export async function bridgeGetHNSWStatus(dbPath) {
1503
1510
  initialized: true,
1504
1511
  entryCount,
1505
1512
  dimensions: 384,
1513
+ algorithm: 'brute-force-cosine',
1506
1514
  };
1507
1515
  }
1508
1516
  catch {
@@ -1510,11 +1518,11 @@ export async function bridgeGetHNSWStatus(dbPath) {
1510
1518
  }
1511
1519
  }
1512
1520
  /**
1513
- * Search using AgentDB v3's embedder + SQLite entries.
1514
- * This is the HNSW-equivalent search through the bridge.
1515
- * Returns null if unavailable.
1521
+ * Search AgentDB v3's embedder + SQLite entries via a full-table scan and
1522
+ * brute-force cosine similarity. NOT HNSW-accelerated see #2922. Returns
1523
+ * null if unavailable.
1516
1524
  */
1517
- export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
1525
+ export async function bridgeSearchBruteForceCosine(queryEmbedding, options, dbPath) {
1518
1526
  const registry = await getRegistry(dbPath);
1519
1527
  if (!registry)
1520
1528
  return null;
@@ -1576,10 +1584,11 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
1576
1584
  }
1577
1585
  }
1578
1586
  /**
1579
- * Add entry to the bridge's database with embedding.
1580
- * Returns null if unavailable.
1587
+ * Add entry to the bridge's database with embedding. No HNSW index is built
1588
+ * or updated here — see #2922; the embedding is only stored for a later
1589
+ * brute-force scan. Returns null if unavailable.
1581
1590
  */
1582
- export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
1591
+ export async function bridgeAddEmbedding(id, embedding, entry, dbPath) {
1583
1592
  const registry = await getRegistry(dbPath);
1584
1593
  if (!registry)
1585
1594
  return null;
@@ -91,6 +91,7 @@ export declare function getHNSWStatus(): {
91
91
  initialized: boolean;
92
92
  entryCount: number;
93
93
  dimensions: number;
94
+ algorithm: 'hnsw' | 'brute-force-cosine';
94
95
  };
95
96
  /**
96
97
  * Clear the HNSW index (for rebuilding)
@@ -750,7 +750,7 @@ export async function addToHNSWIndex(id, embedding, entry) {
750
750
  // ADR-053: Try AgentDB v3 bridge first
751
751
  const bridge = await getBridge();
752
752
  if (bridge) {
753
- const bridgeResult = await bridge.bridgeAddToHNSW(id, embedding, entry);
753
+ const bridgeResult = await bridge.bridgeAddEmbedding(id, embedding, entry);
754
754
  if (bridgeResult === true)
755
755
  return true;
756
756
  }
@@ -780,7 +780,7 @@ export async function searchHNSWIndex(queryEmbedding, options) {
780
780
  // ADR-053: Try AgentDB v3 bridge first
781
781
  const bridge = await getBridge();
782
782
  if (bridge) {
783
- const bridgeResult = await bridge.bridgeSearchHNSW(queryEmbedding, options);
783
+ const bridgeResult = await bridge.bridgeSearchBruteForceCosine(queryEmbedding, options);
784
784
  if (bridgeResult)
785
785
  return bridgeResult;
786
786
  }
@@ -826,14 +826,23 @@ export async function searchHNSWIndex(queryEmbedding, options) {
826
826
  * Get HNSW index status
827
827
  */
828
828
  export function getHNSWStatus() {
829
- // ADR-053: If bridge was previously loaded, report availability
829
+ // #2922: this branch previously reported `available: true` whenever the
830
+ // bridge was loaded, on the theory that "AgentDB v3 is HNSW-equivalent" —
831
+ // but the bridge's actual search path (bridgeSearchBruteForceCosine, see
832
+ // memory-bridge.ts) does a full-table SELECT + brute-force cosine loop and
833
+ // never touches @ruvector/core or an HNSW index, regardless of whether
834
+ // ruvector-core itself is installed. `available` here means "an HNSW
835
+ // index is the one actually searched", matching what this function's own
836
+ // name promises — that's false while the bridge is the active path, so
837
+ // this no longer claims otherwise. Vector search itself still works via
838
+ // the bridge; `algorithm` tells callers it's brute-force, not HNSW.
830
839
  if (_bridge && _bridge !== null) {
831
- // Bridge is loaded — HNSW-equivalent is available via AgentDB v3
832
840
  return {
833
- available: true,
834
- initialized: true,
841
+ available: false,
842
+ initialized: false,
835
843
  entryCount: hnswIndex?.entries.size ?? 0,
836
- dimensions: hnswIndex?.dimensions ?? 384
844
+ dimensions: hnswIndex?.dimensions ?? 384,
845
+ algorithm: 'brute-force-cosine',
837
846
  };
838
847
  }
839
848
  // #2356: `available` now reflects real capability (index already loaded OR
@@ -845,7 +854,8 @@ export function getHNSWStatus() {
845
854
  available: hnswIndex !== null || isRuvectorCoreResolvable(),
846
855
  initialized: hnswIndex?.initialized ?? false,
847
856
  entryCount: hnswIndex?.entries.size ?? 0,
848
- dimensions: hnswIndex?.dimensions ?? 384
857
+ dimensions: hnswIndex?.dimensions ?? 384,
858
+ algorithm: 'hnsw',
849
859
  };
850
860
  }
851
861
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.38.6",
3
+ "version": "3.38.8",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",